first test

This commit is contained in:
Paul Ebbers 2024-11-07 23:16:46 +01:00
parent 09b07037a6
commit bef6f91b3c
12 changed files with 1222 additions and 922 deletions

View File

@ -3,55 +3,70 @@ globalGroups = []
itemGroups = None
serializedItemGroups = None
def onResultSelected(index, groupId):
global globalGroups
nfo = globalGroups[groupId]
handlerName = nfo['action']['handler']
import SearchResults
if handlerName in SearchResults.actionHandlers:
SearchResults.actionHandlers[handlerName](nfo)
else:
from PySide import QtGui
QtGui.QMessageBox.warning(None, 'Could not execute this action', 'Could not execute this action, it could be from a Mod that has been uninstalled. Try refreshing the list of tools.')
global globalGroups
nfo = globalGroups[groupId]
handlerName = nfo["action"]["handler"]
import SearchResults
if handlerName in SearchResults.actionHandlers:
SearchResults.actionHandlers[handlerName](nfo)
else:
from PySide6 import QtGui
QtGui.QMessageBox.warning(
None,
"Could not execute this action",
"Could not execute this action, it could be from a Mod that has been uninstalled. Try refreshing the list of tools.",
)
def getToolTip(groupId, setParent):
global globalGroups
nfo = globalGroups[int(groupId)]
handlerName = nfo['action']['handler']
import SearchResults
if handlerName in SearchResults.toolTipHandlers:
return SearchResults.toolTipHandlers[handlerName](nfo, setParent)
else:
return 'Could not load tooltip for this tool, it could be from a Mod that has been uninstalled. Try refreshing the list of tools.'
global globalGroups
nfo = globalGroups[int(groupId)]
handlerName = nfo["action"]["handler"]
import SearchResults
if handlerName in SearchResults.toolTipHandlers:
return SearchResults.toolTipHandlers[handlerName](nfo, setParent)
else:
return "Could not load tooltip for this tool, it could be from a Mod that has been uninstalled. Try refreshing the list of tools."
def getItemGroups():
global itemGroups, serializedItemGroups, globalGroups
# Import the tooltip+action handlers and search result providers that are bundled with this Mod.
# Other providers should import SearchResults and register their handlers and providers
import BuiltInSearchResults
global itemGroups, serializedItemGroups, globalGroups
# Load the list of tools, preferably from the cache, if it has not already been loaded:
if itemGroups is None:
if serializedItemGroups is None:
import RefreshTools
itemGroups = RefreshTools.refreshToolbars(doLoadAllWorkbenches = False)
else:
import Serialize
itemGroups = Serialize.deserialize(serializedItemGroups)
# Import the tooltip+action handlers and search result providers that are bundled with this Mod.
# Other providers should import SearchResults and register their handlers and providers
import BuiltInSearchResults
# Aggregate the tools (cached) and document objects (not cached), and assign an index to each
import SearchResults
igs = itemGroups
for providerName, provider in SearchResults.resultProvidersUncached.items():
igs = igs + provider()
globalGroups = []
def addId(group):
globalGroups.append(group)
group['id'] = len(globalGroups) - 1
for subitem in group['subitems']:
addId(subitem)
for ig in igs:
addId(ig)
return igs
# Load the list of tools, preferably from the cache, if it has not already been loaded:
if itemGroups is None:
if serializedItemGroups is None:
import RefreshTools
itemGroups = RefreshTools.refreshToolbars(doLoadAllWorkbenches=False)
else:
import Serialize
itemGroups = Serialize.deserialize(serializedItemGroups)
# Aggregate the tools (cached) and document objects (not cached), and assign an index to each
import SearchResults
igs = itemGroups
for providerName, provider in SearchResults.resultProvidersUncached.items():
igs = igs + provider()
globalGroups = []
def addId(group):
globalGroups.append(group)
group["id"] = len(globalGroups) - 1
for subitem in group["subitems"]:
addId(subitem)
for ig in igs:
addId(ig)
return igs

View File

@ -1,12 +1,14 @@
from PySide import QtGui
from PySide6 import QtGui
# Inspired by https://stackoverflow.com/a/5443220/324969
# Inspired by https://forum.qt.io/topic/69807/qtreeview-indent-entire-row
class IndentedItemDelegate(QtGui.QStyledItemDelegate):
def __init__(self):
super(IndentedItemDelegate, self).__init__()
def paint(self, painter, option, index):
depth = int(option.widget.model().itemData(index.siblingAtColumn(1))[0])
indent = 16 * depth
option.rect.adjust(indent, 0, 0, 0)
super(IndentedItemDelegate, self).paint(painter, option, index)
def __init__(self):
super(IndentedItemDelegate, self).__init__()
def paint(self, painter, option, index):
depth = int(option.widget.model().itemData(index.siblingAtColumn(1))[0])
indent = 16 * depth
option.rect.adjust(indent, 0, 0, 0)
super(IndentedItemDelegate, self).paint(painter, option, index)

View File

@ -3,35 +3,43 @@ wax = None
sea = None
tbr = None
def addToolSearchBox():
import FreeCADGui
from PySide import QtGui
import SearchBoxLight
global wax, sea, tbr
mw = FreeCADGui.getMainWindow()
if mw:
if sea is None:
sea = SearchBoxLight.SearchBoxLight(getItemGroups = lambda: __import__('GetItemGroups').getItemGroups(),
getToolTip = lambda groupId, setParent: __import__('GetItemGroups').getToolTip(groupId, setParent),
getItemDelegate = lambda: __import__('IndentedItemDelegate').IndentedItemDelegate())
sea.resultSelected.connect(lambda index, groupId: __import__('GetItemGroups').onResultSelected(index, groupId))
import FreeCADGui
from PySide6 import QtGui
import SearchBoxLight
if wax is None:
wax = QtGui.QWidgetAction(None)
wax.setWhatsThis('Use this search bar to find tools, document objects, preferences and more')
global wax, sea, tbr
mw = FreeCADGui.getMainWindow()
if mw:
if sea is None:
sea = SearchBoxLight.SearchBoxLight(
getItemGroups=lambda: __import__("GetItemGroups").getItemGroups(),
getToolTip=lambda groupId, setParent: __import__("GetItemGroups").getToolTip(groupId, setParent),
getItemDelegate=lambda: __import__("IndentedItemDelegate").IndentedItemDelegate(),
)
sea.resultSelected.connect(
lambda index, groupId: __import__("GetItemGroups").onResultSelected(index, groupId)
)
if wax is None:
wax = QtGui.QWidgetAction(None)
wax.setWhatsThis("Use this search bar to find tools, document objects, preferences and more")
sea.setWhatsThis("Use this search bar to find tools, document objects, preferences and more")
wax.setDefaultWidget(sea)
##mbr.addWidget(sea)
# mbr.addAction(wax)
if tbr is None:
tbr = QtGui.QToolBar("SearchBar") # QtGui.QDockWidget()
# Include FreeCAD in the name so that one can find windows labeled with FreeCAD easily in window managers which allow search through the list of open windows.
tbr.setObjectName("SearchBar")
tbr.addAction(wax)
mw.addToolBar(tbr)
tbr.show()
sea.setWhatsThis('Use this search bar to find tools, document objects, preferences and more')
wax.setDefaultWidget(sea)
##mbr.addWidget(sea)
#mbr.addAction(wax)
if tbr is None:
tbr = QtGui.QToolBar("SearchBar") #QtGui.QDockWidget()
# Include FreeCAD in the name so that one can find windows labeled with FreeCAD easily in window managers which allow search through the list of open windows.
tbr.setObjectName("SearchBar")
tbr.addAction(wax)
mw.addToolBar(tbr)
tbr.show()
addToolSearchBox()
import FreeCADGui
FreeCADGui.getMainWindow().workbenchActivated.connect(addToolSearchBox)

View File

@ -1,81 +1,99 @@
import os
import FreeCAD as App
def loadAllWorkbenches():
from PySide import QtGui
import FreeCADGui
activeWorkbench = FreeCADGui.activeWorkbench().name()
lbl = QtGui.QLabel('Loading workbench … (…/…)')
lbl.show()
lst = FreeCADGui.listWorkbenches()
for i, wb in enumerate(lst):
msg = 'Loading workbench ' + wb + ' (' + str(i) + '/' + str(len(lst)) + ')'
print(msg)
lbl.setText(msg)
geo = lbl.geometry()
geo.setSize(lbl.sizeHint())
lbl.setGeometry(geo)
lbl.repaint()
FreeCADGui.updateGui() # Probably slower with this, because it redraws the entire GUI with all tool buttons changed etc. but allows the label to actually be updated, and it looks nice and gives a quick overview of all the workbenches…
try:
FreeCADGui.activateWorkbench(wb)
except:
pass
lbl.hide()
FreeCADGui.activateWorkbench(activeWorkbench)
from PySide6 import QtGui
import FreeCADGui
activeWorkbench = FreeCADGui.activeWorkbench().name()
lbl = QtGui.QLabel("Loading workbench … (…/…)")
lbl.show()
lst = FreeCADGui.listWorkbenches()
for i, wb in enumerate(lst):
msg = "Loading workbench " + wb + " (" + str(i) + "/" + str(len(lst)) + ")"
print(msg)
lbl.setText(msg)
geo = lbl.geometry()
geo.setSize(lbl.sizeHint())
lbl.setGeometry(geo)
lbl.repaint()
FreeCADGui.updateGui() # Probably slower with this, because it redraws the entire GUI with all tool buttons changed etc. but allows the label to actually be updated, and it looks nice and gives a quick overview of all the workbenches…
try:
FreeCADGui.activateWorkbench(wb)
except:
pass
lbl.hide()
FreeCADGui.activateWorkbench(activeWorkbench)
def cachePath():
return os.path.join(App.getUserAppDataDir(), 'Cache_SearchBarMod')
return os.path.join(App.getUserAppDataDir(), "Cache_SearchBarMod")
def gatherTools():
itemGroups = []
import SearchResults
for providerName, provider in SearchResults.resultProvidersCached.items():
itemGroups = itemGroups + provider()
return itemGroups
itemGroups = []
import SearchResults
for providerName, provider in SearchResults.resultProvidersCached.items():
itemGroups = itemGroups + provider()
return itemGroups
def writeCacheTools():
import Serialize
serializedItemGroups = Serialize.serialize(gatherTools())
# Todo: use wb and a specific encoding.
with open(cachePath(), 'w') as cache:
cache.write(serializedItemGroups)
# I prefer to systematically deserialize, instead of taking the original version,
# this avoids possible inconsistencies between the original and the cache and
# makes sure cache-related bugs are noticed quickly.
import Serialize
itemGroups = Serialize.deserialize(serializedItemGroups)
print('SearchBox: Cache has been written.')
return itemGroups
import Serialize
serializedItemGroups = Serialize.serialize(gatherTools())
# Todo: use wb and a specific encoding.
with open(cachePath(), "w") as cache:
cache.write(serializedItemGroups)
# I prefer to systematically deserialize, instead of taking the original version,
# this avoids possible inconsistencies between the original and the cache and
# makes sure cache-related bugs are noticed quickly.
import Serialize
itemGroups = Serialize.deserialize(serializedItemGroups)
print("SearchBox: Cache has been written.")
return itemGroups
def readCacheTools():
# Todo: use rb and a specific encoding.
with open(cachePath(), 'r') as cache:
serializedItemGroups = cache.read()
import Serialize
itemGroups = Serialize.deserialize(serializedItemGroups)
print('SearchBox: Tools were loaded from the cache.')
return itemGroups
# Todo: use rb and a specific encoding.
with open(cachePath(), "r") as cache:
serializedItemGroups = cache.read()
import Serialize
itemGroups = Serialize.deserialize(serializedItemGroups)
print("SearchBox: Tools were loaded from the cache.")
return itemGroups
def refreshToolbars(doLoadAllWorkbenches = True):
if doLoadAllWorkbenches:
loadAllWorkbenches()
return writeCacheTools()
else:
try:
return readCacheTools()
except:
return writeCacheTools()
def refreshToolbars(doLoadAllWorkbenches=True):
if doLoadAllWorkbenches:
loadAllWorkbenches()
return writeCacheTools()
else:
try:
return readCacheTools()
except:
return writeCacheTools()
def refreshToolsAction():
from PySide import QtGui
print('Refresh list of tools')
fw = QtGui.QApplication.focusWidget()
if fw is not None:
fw.clearFocus()
reply = QtGui.QMessageBox.question(None, "Load all workbenches?", "Load all workbenches? This can cause FreeCAD to become unstable, and this \"reload tools\" feature contained a bug that crashed freecad systematically, so please make sure you save your work before. It's a good idea to restart FreeCAD after this operation.", QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)
if reply == QtGui.QMessageBox.Yes:
refreshToolbars()
else:
print('cancelled')
from PySide6 import QtGui
print("Refresh list of tools")
fw = QtGui.QApplication.focusWidget()
if fw is not None:
fw.clearFocus()
reply = QtGui.QMessageBox.question(
None,
"Load all workbenches?",
'Load all workbenches? This can cause FreeCAD to become unstable, and this "reload tools" feature contained a bug that crashed freecad systematically, so please make sure you save your work before. It\'s a good idea to restart FreeCAD after this operation.',
QtGui.QMessageBox.Yes,
QtGui.QMessageBox.No,
)
if reply == QtGui.QMessageBox.Yes:
refreshToolbars()
else:
print("cancelled")

View File

@ -1,116 +1,143 @@
from PySide import QtGui
from PySide import QtCore
from PySide6 import QtGui
from PySide6 import QtCore
import FreeCAD as App
import FreeCADGui
import SafeViewer
import SearchBox
def documentAction(nfo):
act = nfo['action']
act = nfo["action"]
# Todo: this should also select the document in the tree view
print('switch to document ' + act['document'])
App.setActiveDocument(act['document'])
print("switch to document " + act["document"])
App.setActiveDocument(act["document"])
def documentObjectAction(nfo):
act = nfo['action']
print('select object ' + act['document'] + '.' + act['object'])
FreeCADGui.Selection.addSelection(act['document'], act['object'])
act = nfo["action"]
print("select object " + act["document"] + "." + act["object"])
FreeCADGui.Selection.addSelection(act["document"], act["object"])
# For some reason, the viewer always works except when used for two consecutive items in the search results: it then disappears after a short zoom-in+zoom-out animation.
# I'm giving up on getting this viewer to work in a clean way, and will try swapping two instances so that the same one is never used twice in a row.
# Also, in order to avoid segfaults when the module is reloaded (which causes the previous viewer to be garbage collected at some point), we're using a global property that will survive module reloads.
if not hasattr(App, '_SearchBar3DViewer'):
# Toggle between
App._SearchBar3DViewer = None
App._SearchBar3DViewerB = None
if not hasattr(App, "_SearchBar3DViewer"):
# Toggle between
App._SearchBar3DViewer = None
App._SearchBar3DViewerB = None
class DocumentObjectToolTipWidget(QtGui.QWidget):
def __init__(self, nfo, setParent):
import pivy
super(DocumentObjectToolTipWidget, self).__init__()
html = '<p>' + nfo['toolTip']['label'] + '</p><p><code>App.getDocument(' + repr(str(nfo['toolTip']['docName'])) + ').getObject(' + repr(str(nfo['toolTip']['name'])) + ')</code></p>'
description = QtGui.QTextEdit()
description.setReadOnly(True)
description.setAlignment(QtCore.Qt.AlignTop)
description.setText(html)
def __init__(self, nfo, setParent):
import pivy
if App._SearchBar3DViewer is None:
oldFocus = QtGui.QApplication.focusWidget()
SearchBox.globalIgnoreFocusOut
SearchBox.globalIgnoreFocusOut = True
App._SearchBar3DViewer = SafeViewer.SafeViewer()
App._SearchBar3DViewerB = SafeViewer.SafeViewer()
oldFocus.setFocus()
SearchBox.globalIgnoreFocusOut = False
# Tried setting the preview to a fixed size to prevent it from disappearing when changing its contents, this sets it to a fixed size but doesn't actually pick the size, .resize does that but isn't enough to fix the bug.
#safeViewerInstance.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed))
self.preview = App._SearchBar3DViewer
App._SearchBar3DViewer, App._SearchBar3DViewerB = App._SearchBar3DViewerB, App._SearchBar3DViewer
super(DocumentObjectToolTipWidget, self).__init__()
html = (
"<p>"
+ nfo["toolTip"]["label"]
+ "</p><p><code>App.getDocument("
+ repr(str(nfo["toolTip"]["docName"]))
+ ").getObject("
+ repr(str(nfo["toolTip"]["name"]))
+ ")</code></p>"
)
description = QtGui.QTextEdit()
description.setReadOnly(True)
description.setAlignment(QtCore.Qt.AlignTop)
description.setText(html)
obj = App.getDocument(str(nfo['toolTip']['docName'])).getObject(str(nfo['toolTip']['name']))
if App._SearchBar3DViewer is None:
oldFocus = QtGui.QApplication.focusWidget()
SearchBox.globalIgnoreFocusOut
SearchBox.globalIgnoreFocusOut = True
App._SearchBar3DViewer = SafeViewer.SafeViewer()
App._SearchBar3DViewerB = SafeViewer.SafeViewer()
oldFocus.setFocus()
SearchBox.globalIgnoreFocusOut = False
# Tried setting the preview to a fixed size to prevent it from disappearing when changing its contents, this sets it to a fixed size but doesn't actually pick the size, .resize does that but isn't enough to fix the bug.
# safeViewerInstance.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed))
self.preview = App._SearchBar3DViewer
App._SearchBar3DViewer, App._SearchBar3DViewerB = App._SearchBar3DViewerB, App._SearchBar3DViewer
# This is really a bad way to do this… to prevent the setExtraInfo function from
# finalizing the object, we remove the parent ourselves.
oldParent = self.preview.parent()
lay = QtGui.QVBoxLayout()
lay.setContentsMargins(0,0,0,0)
lay.setSpacing(0)
self.setLayout(lay)
lay.addWidget(description)
lay.addWidget(self.preview)
#if oldParent is not None:
# oldParent.hide() # hide before detaching, or we have widgets floating as their own window that appear for a split second in some cases.
# oldParent.setParent(None)
# Tried hiding/detaching the preview to prevent it from disappearing when changing its contents
#self.preview.viewer.stopAnimating()
self.preview.showSceneGraph(obj.ViewObject.RootNode)
obj = App.getDocument(str(nfo["toolTip"]["docName"])).getObject(str(nfo["toolTip"]["name"]))
setParent(self)
# Let the GUI recompute the side of the description based on its horizontal size.
FreeCADGui.updateGui()
siz = description.document().size().toSize()
description.setFixedHeight(siz.height() + 5)
# This is really a bad way to do this… to prevent the setExtraInfo function from
# finalizing the object, we remove the parent ourselves.
oldParent = self.preview.parent()
lay = QtGui.QVBoxLayout()
lay.setContentsMargins(0, 0, 0, 0)
lay.setSpacing(0)
self.setLayout(lay)
lay.addWidget(description)
lay.addWidget(self.preview)
# if oldParent is not None:
# oldParent.hide() # hide before detaching, or we have widgets floating as their own window that appear for a split second in some cases.
# oldParent.setParent(None)
# Tried hiding/detaching the preview to prevent it from disappearing when changing its contents
# self.preview.viewer.stopAnimating()
self.preview.showSceneGraph(obj.ViewObject.RootNode)
setParent(self)
# Let the GUI recompute the side of the description based on its horizontal size.
FreeCADGui.updateGui()
siz = description.document().size().toSize()
description.setFixedHeight(siz.height() + 5)
def finalizer(self):
# self.preview.finalizer()
# Detach the widget so that it may be reused without getting deleted
self.preview.setParent(None)
def finalizer(self):
#self.preview.finalizer()
# Detach the widget so that it may be reused without getting deleted
self.preview.setParent(None)
def documentToolTip(nfo, setParent):
return '<p>' + nfo['toolTip']['label'] + '</p><p><code>App.getDocument(' + repr(str(nfo['toolTip']['name'])) + ')</code></p><p><img src="data:image/png;base64,.............."></p>'
return (
"<p>"
+ nfo["toolTip"]["label"]
+ "</p><p><code>App.getDocument("
+ repr(str(nfo["toolTip"]["name"]))
+ ')</code></p><p><img src="data:image/png;base64,.............."></p>'
)
def documentObjectToolTip(nfo, setParent):
return DocumentObjectToolTipWidget(nfo, setParent)
return DocumentObjectToolTipWidget(nfo, setParent)
def documentResultsProvider():
itemGroups = []
def document(doc):
group = []
for o in doc.Objects:
#all_actions.append(lambda: )
action = { 'handler': 'documentObject', 'document': o.Document.Name, 'object': o.Name }
item = {
'icon': o.ViewObject.Icon if o.ViewObject and o.ViewObject.Icon else None,
'text': o.Label + ' (' + o.Name + ')',
# TODO: preview of the object
'toolTip': { 'label': o.Label, 'name': o.Name, 'docName': o.Document.Name},
'action': action,
'subitems': []
}
group.append(item)
itemGroups = []
action = { 'handler': 'document', 'document': doc.Name }
itemGroups.append({
'icon': QtGui.QIcon(':/icons/Document.svg'),
'text': doc.Label + ' (' + doc.Name + ')',
# TODO: preview of the document
'toolTip': { 'label': doc.Label, 'name': doc.Name},
'action':action,
'subitems': group })
if App.ActiveDocument:
document(App.ActiveDocument)
for docname, doc in App.listDocuments().items():
if not App.activeDocument or docname != App.ActiveDocument.Name:
document(doc)
return itemGroups
def document(doc):
group = []
for o in doc.Objects:
# all_actions.append(lambda: )
action = {"handler": "documentObject", "document": o.Document.Name, "object": o.Name}
item = {
"icon": o.ViewObject.Icon if o.ViewObject and o.ViewObject.Icon else None,
"text": o.Label + " (" + o.Name + ")",
# TODO: preview of the object
"toolTip": {"label": o.Label, "name": o.Name, "docName": o.Document.Name},
"action": action,
"subitems": [],
}
group.append(item)
action = {"handler": "document", "document": doc.Name}
itemGroups.append(
{
"icon": QtGui.QIcon(":/icons/Document.svg"),
"text": doc.Label + " (" + doc.Name + ")",
# TODO: preview of the document
"toolTip": {"label": doc.Label, "name": doc.Name},
"action": action,
"subitems": group,
}
)
if App.ActiveDocument:
document(App.ActiveDocument)
for docname, doc in App.listDocuments().items():
if not App.activeDocument or docname != App.ActiveDocument.Name:
document(doc)
return itemGroups

View File

@ -1,90 +1,122 @@
import os
import FreeCAD as App
import FreeCADGui
from PySide import QtGui
from PySide6 import QtGui
import Serialize
genericToolIcon = QtGui.QIcon(QtGui.QIcon(os.path.dirname(__file__) + '/Tango-Tools-spanner-hammer.svg'))
genericToolIcon = QtGui.QIcon(QtGui.QIcon(os.path.dirname(__file__) + "/Tango-Tools-spanner-hammer.svg"))
def getParam(grpPath, type_, name):
return {
# TODO: use letter icon based on the type, as the preferences editor does
'icon': genericToolIcon,
'text': name,
'toolTip': '',
'action': {'handler': 'param', 'path': grpPath, 'type': type_, 'name': name},
'subitems': []
}
return {
# TODO: use letter icon based on the type, as the preferences editor does
"icon": genericToolIcon,
"text": name,
"toolTip": "",
"action": {"handler": "param", "path": grpPath, "type": type_, "name": name},
"subitems": [],
}
def getParamGroup(grpPath):
try:
grp = App.ParamGet(grpPath)
except:
return []
contents = grp.GetContents()
if contents is not None:
return [getParam(grpPath, type_, name) for (type_, name, value) in contents]
else:
return []
try:
grp = App.ParamGet(grpPath)
except:
return []
contents = grp.GetContents()
if contents is not None:
return [getParam(grpPath, type_, name) for (type_, name, value) in contents]
else:
return []
def getParamGroups(nameInConfig, nameInPath):
userParameterPath = App.ConfigGet(nameInConfig)
from lxml import etree
xml = etree.parse(userParameterPath).getroot()
xml.find('FCParamGroup[@Name="Root"]')
root = xml.find('FCParamGroup[@Name="Root"]')
def recur(atRoot, path, name, tree):
params = [] if atRoot else getParamGroup(path)
subgroups = [recur(False, path + (':' if atRoot else '/') + child.attrib['Name'], child.attrib['Name'], child) for child in tree.getchildren() if child.tag == 'FCParamGroup']
return {
'icon': QtGui.QIcon(':/icons/Group.svg'),
'text': name,
'toolTip': '',
'action': { 'handler': 'paramGroup', 'path': path, 'name': name },
'subitems': params + subgroups
}
params = [] if atRoot else getParamGroup(path)
subgroups = [
recur(False, path + (":" if atRoot else "/") + child.attrib["Name"], child.attrib["Name"], child)
for child in tree.getchildren()
if child.tag == "FCParamGroup"
]
return {
"icon": QtGui.QIcon(":/icons/Group.svg"),
"text": name,
"toolTip": "",
"action": {"handler": "paramGroup", "path": path, "name": name},
"subitems": params + subgroups,
}
return recur(True, nameInPath, nameInPath, root)
def getAllParams():
try:
from lxml import etree
return [getParamGroups('UserParameter', 'User parameter')]
except:
print('Could not load the list of all parameters. Please install the LXML python library with:\npython -m pip install --upgrade lxml')
return []
try:
from lxml import etree
return [getParamGroups("UserParameter", "User parameter")]
except:
print(
"Could not load the list of all parameters. Please install the LXML python library with:\npython -m pip install --upgrade lxml"
)
return []
def paramGroupAction(nfo):
FreeCADGui.runCommand('Std_DlgParameter',0)
print('Open Parameter Editor (parameter group)')
# TODO: find a way to select the desired group in the parameter dialog once it opens
FreeCADGui.runCommand("Std_DlgParameter", 0)
print("Open Parameter Editor (parameter group)")
# TODO: find a way to select the desired group in the parameter dialog once it opens
def paramAction(nfo):
FreeCADGui.runCommand('Std_DlgParameter',0)
print('Open Parameter Editor (single parameter)')
# TODO: find a way to select the desired parameter in the parameter dialog once it opens
FreeCADGui.runCommand("Std_DlgParameter", 0)
print("Open Parameter Editor (single parameter)")
# TODO: find a way to select the desired parameter in the parameter dialog once it opens
getters = {
'Boolean' : 'GetBool',
'Float' : 'GetFloat',
'Integer' : 'GetInt',
'String' : 'GetString',
'Unsigned Long': 'GetUnsigned',
"Boolean": "GetBool",
"Float": "GetFloat",
"Integer": "GetInt",
"String": "GetString",
"Unsigned Long": "GetUnsigned",
}
def paramGroupToolTip(nfo, setParent):
path = nfo['action']['path']
name = nfo['action']['name']
return '<h1>' + name + '</h1><p><code>App.ParamGet(' + repr(path) + ')</code></p'
path = nfo["action"]["path"]
name = nfo["action"]["name"]
return "<h1>" + name + "</h1><p><code>App.ParamGet(" + repr(path) + ")</code></p"
def paramToolTip(nfo, setParent):
path = nfo['action']['path']
type_ = nfo['action']['type']
name = nfo['action']['name']
try:
value = getattr(App.ParamGet(path), getters[type_])(name)
except:
value = 'An error occurred while attempting to access this value.'
return '<p><code>App.ParamGet(' + repr(path) + ').' + getters[type_] + '(' + repr(name) + ')</code></p><p>Type: ' + type_ + '</p><p>Value: ' + repr(value) + '</p>'
path = nfo["action"]["path"]
type_ = nfo["action"]["type"]
name = nfo["action"]["name"]
try:
value = getattr(App.ParamGet(path), getters[type_])(name)
except:
value = "An error occurred while attempting to access this value."
return (
"<p><code>App.ParamGet("
+ repr(path)
+ ")."
+ getters[type_]
+ "("
+ repr(name)
+ ")</code></p><p>Type: "
+ type_
+ "</p><p>Value: "
+ repr(value)
+ "</p>"
)
def paramResultsProvider():
return getAllParams()
return getAllParams()

View File

@ -1,22 +1,31 @@
import os
from PySide import QtGui
from PySide6 import QtGui
import Serialize
def refreshToolsAction(nfo):
import RefreshTools
RefreshTools.refreshToolsAction()
import RefreshTools
RefreshTools.refreshToolsAction()
def refreshToolsToolTip(nfo, setParent):
return Serialize.iconToHTML(genericToolIcon) + '<p>Load all workbenches to refresh this list of tools. This may take a minute, depending on the number of installed workbenches.</p>'
return (
Serialize.iconToHTML(genericToolIcon)
+ "<p>Load all workbenches to refresh this list of tools. This may take a minute, depending on the number of installed workbenches.</p>"
)
genericToolIcon = QtGui.QIcon(QtGui.QIcon(os.path.dirname(__file__) + "/Tango-Tools-spanner-hammer.svg"))
genericToolIcon = QtGui.QIcon(QtGui.QIcon(os.path.dirname(__file__) + '/Tango-Tools-spanner-hammer.svg'))
def refreshToolsResultsProvider():
return [
{
'icon': genericToolIcon,
'text': 'Refresh list of tools',
'toolTip': '',
'action': {'handler': 'refreshTools'},
'subitems': []
}
]
return [
{
"icon": genericToolIcon,
"text": "Refresh list of tools",
"toolTip": "",
"action": {"handler": "refreshTools"},
"subitems": [],
}
]

View File

@ -1,105 +1,153 @@
from PySide import QtGui
from PySide6 import QtGui
import FreeCADGui
import Serialize
def toolbarAction(nfo):
act = nfo['action']
print('show toolbar ' + act['toolbar'] + ' from workbenches ' + repr(act['workbenches']))
act = nfo["action"]
print("show toolbar " + act["toolbar"] + " from workbenches " + repr(act["workbenches"]))
def subToolAction(nfo):
act = nfo['action']
toolPath = act['toolbar'] + '.' + act['tool']
if 'subTool' in act:
toolPath = toolPath + '.' + act['subTool']
def runTool():
mw = FreeCADGui.getMainWindow()
for the_toolbar in mw.findChildren(QtGui.QToolBar, act['toolbar']):
for tbt in the_toolbar.findChildren(QtGui.QToolButton):
if tbt.text() == act['tool']:
action = None
if 'subTool' in act:
men = tbt.menu()
if men:
for mac in men.actions():
if mac.text() == act['subTool']:
action = mac
break
else:
action = tbt.defaultAction()
if 'showMenu' in act and act['showMenu']:
print('Popup submenu of tool ' + toolPath + ' available in workbenches ' + repr(act['workbenches']))
the_toolbar.show()
tbt.showMenu()
return True
elif action is not None:
print('Run action of tool ' + toolPath + ' available in workbenches ' + repr(act['workbenches']))
action.trigger()
return True
return False
if runTool():
return
else:
for workbench in act['workbenches']:
print('Activating workbench ' + workbench + ' to access tool ' + toolPath)
FreeCADGui.activateWorkbench(workbench)
if runTool():
act = nfo["action"]
toolPath = act["toolbar"] + "." + act["tool"]
if "subTool" in act:
toolPath = toolPath + "." + act["subTool"]
def runTool():
mw = FreeCADGui.getMainWindow()
for the_toolbar in mw.findChildren(QtGui.QToolBar, act["toolbar"]):
for tbt in the_toolbar.findChildren(QtGui.QToolButton):
if tbt.text() == act["tool"]:
action = None
if "subTool" in act:
men = tbt.menu()
if men:
for mac in men.actions():
if mac.text() == act["subTool"]:
action = mac
break
else:
action = tbt.defaultAction()
if "showMenu" in act and act["showMenu"]:
print(
"Popup submenu of tool "
+ toolPath
+ " available in workbenches "
+ repr(act["workbenches"])
)
the_toolbar.show()
tbt.showMenu()
return True
elif action is not None:
print(
"Run action of tool " + toolPath + " available in workbenches " + repr(act["workbenches"])
)
action.trigger()
return True
return False
if runTool():
return
print('Tool ' + toolPath + ' not found, was it offered by an extension that is no longer present?')
else:
for workbench in act["workbenches"]:
print("Activating workbench " + workbench + " to access tool " + toolPath)
FreeCADGui.activateWorkbench(workbench)
if runTool():
return
print("Tool " + toolPath + " not found, was it offered by an extension that is no longer present?")
def toolbarToolTip(nfo, setParent):
workbenches = FreeCADGui.listWorkbenches()
in_workbenches = ['<li>' + (Serialize.iconToHTML(QtGui.QIcon(workbenches[wb].Icon)) if wb in workbenches else '? ') + wb + '</li>' for wb in nfo['action']['workbenches']]
return '<p>Show the ' + nfo['text'] + ' toolbar</p><p>This toolbar appears in the following workbenches: <ul>' + ''.join(in_workbenches) + '</ul></p>'
workbenches = FreeCADGui.listWorkbenches()
in_workbenches = [
"<li>" + (Serialize.iconToHTML(QtGui.QIcon(workbenches[wb].Icon)) if wb in workbenches else "? ") + wb + "</li>"
for wb in nfo["action"]["workbenches"]
]
return (
"<p>Show the "
+ nfo["text"]
+ " toolbar</p><p>This toolbar appears in the following workbenches: <ul>"
+ "".join(in_workbenches)
+ "</ul></p>"
)
def subToolToolTip(nfo, setParent):
return Serialize.iconToHTML(nfo['icon'], 32) + '<p>' + nfo['toolTip'] + '</p>'
return Serialize.iconToHTML(nfo["icon"], 32) + "<p>" + nfo["toolTip"] + "</p>"
def getAllToolbars():
all_tbs = dict()
for wbname, workbench in FreeCADGui.listWorkbenches().items():
try:
tbs = workbench.listToolbars()
except:
continue
# careful, tbs contains all the toolbars of the workbench, including shared toolbars
for tb in tbs:
if tb not in all_tbs:
all_tbs[tb] = set()
all_tbs[tb].add(wbname)
return all_tbs
all_tbs = dict()
for wbname, workbench in FreeCADGui.listWorkbenches().items():
try:
tbs = workbench.listToolbars()
except:
continue
# careful, tbs contains all the toolbars of the workbench, including shared toolbars
for tb in tbs:
if tb not in all_tbs:
all_tbs[tb] = set()
all_tbs[tb].add(wbname)
return all_tbs
def toolbarResultsProvider():
itemGroups = []
all_tbs = getAllToolbars()
mw = FreeCADGui.getMainWindow()
for toolbarName, toolbarIsInWorkbenches in all_tbs.items():
toolbarIsInWorkbenches = sorted(list(toolbarIsInWorkbenches))
for the_toolbar in mw.findChildren(QtGui.QToolBar, toolbarName):
group = []
for tbt in the_toolbar.findChildren(QtGui.QToolButton):
text = tbt.text()
act = tbt.defaultAction()
if text != '':
# TODO: there also is the tooltip
icon = tbt.icon()
men = tbt.menu()
subgroup = []
if men:
subgroup = []
for mac in men.actions():
if mac.text():
action = { 'handler': 'subTool', 'workbenches': toolbarIsInWorkbenches, 'toolbar': toolbarName, 'tool': text, 'subTool': mac.text() }
subgroup.append({'icon':mac.icon(), 'text':mac.text(), 'toolTip': mac.toolTip(), 'action':action, 'subitems':[]})
# The default action of a menu changes dynamically, instead of triggering the last action, just show the menu.
action = { 'handler': 'tool', 'workbenches': toolbarIsInWorkbenches, 'toolbar': toolbarName, 'tool': text, 'showMenu': bool(men) }
group.append({'icon':icon, 'text':text, 'toolTip': tbt.toolTip(), 'action': action, 'subitems': subgroup})
# TODO: move the 'workbenches' field to the itemgroup
action = { 'handler': 'toolbar', 'workbenches': toolbarIsInWorkbenches, 'toolbar': toolbarName }
itemGroups.append({
'icon': QtGui.QIcon(':/icons/Group.svg'),
'text': toolbarName,
'toolTip': '',
'action': action,
'subitems': group
})
return itemGroups
itemGroups = []
all_tbs = getAllToolbars()
mw = FreeCADGui.getMainWindow()
for toolbarName, toolbarIsInWorkbenches in all_tbs.items():
toolbarIsInWorkbenches = sorted(list(toolbarIsInWorkbenches))
for the_toolbar in mw.findChildren(QtGui.QToolBar, toolbarName):
group = []
for tbt in the_toolbar.findChildren(QtGui.QToolButton):
text = tbt.text()
act = tbt.defaultAction()
if text != "":
# TODO: there also is the tooltip
icon = tbt.icon()
men = tbt.menu()
subgroup = []
if men:
subgroup = []
for mac in men.actions():
if mac.text():
action = {
"handler": "subTool",
"workbenches": toolbarIsInWorkbenches,
"toolbar": toolbarName,
"tool": text,
"subTool": mac.text(),
}
subgroup.append(
{
"icon": mac.icon(),
"text": mac.text(),
"toolTip": mac.toolTip(),
"action": action,
"subitems": [],
}
)
# The default action of a menu changes dynamically, instead of triggering the last action, just show the menu.
action = {
"handler": "tool",
"workbenches": toolbarIsInWorkbenches,
"toolbar": toolbarName,
"tool": text,
"showMenu": bool(men),
}
group.append(
{"icon": icon, "text": text, "toolTip": tbt.toolTip(), "action": action, "subitems": subgroup}
)
# TODO: move the 'workbenches' field to the itemgroup
action = {"handler": "toolbar", "workbenches": toolbarIsInWorkbenches, "toolbar": toolbarName}
itemGroups.append(
{
"icon": QtGui.QIcon(":/icons/Group.svg"),
"text": toolbarName,
"toolTip": "",
"action": action,
"subitems": group,
}
)
return itemGroups

View File

@ -1,110 +1,120 @@
from PySide import QtGui
from PySide6 import QtGui
import FreeCAD
class SafeViewer(QtGui.QWidget):
"""FreeCAD uses a modified version of QuarterWidget, so the import pivy.quarter one will cause segfaults.
FreeCAD's FreeCADGui.createViewer() puts the viewer widget inside an MDI window, and detaching it without causing segfaults on exit is tricky.
This class contains some kludges to extract the viewer as a standalone widget and destroy it safely."""
enabled = FreeCAD.ParamGet('User parameter:BaseApp/Preferences/Mod/SearchBar').GetBool('PreviewEnabled', False)
instances = []
def __init__(self, parent = None):
super(SafeViewer, self).__init__()
SafeViewer.instances.append(self)
self.init_parent = parent
self.instance_enabled = False # Has this specific instance been enabled?
if SafeViewer.enabled:
self.displaying_warning = False
self.enable()
else:
import FreeCADGui
from PySide import QtCore
self.displaying_warning = True
self.lbl_warning = QtGui.QTextEdit()
self.lbl_warning.setReadOnly(True)
self.lbl_warning.setAlignment(QtCore.Qt.AlignTop)
self.lbl_warning.setText("Warning: the 3D preview has some stability issues. It can cause FreeCAD to crash (usually when quitting the application) and could in theory cause data loss, inside and outside of FreeCAD.")
self.btn_enable_for_this_session = QtGui.QPushButton('Enable 3D preview for this session')
self.btn_enable_for_this_session.clicked.connect(self.enable_for_this_session)
self.btn_enable_for_future_sessions = QtGui.QPushButton('Enable 3D preview for future sessions')
self.btn_enable_for_future_sessions.clicked.connect(self.enable_for_future_sessions)
self.setLayout(QtGui.QVBoxLayout())
self.layout().addWidget(self.lbl_warning)
self.layout().addWidget(self.btn_enable_for_this_session)
self.layout().addWidget(self.btn_enable_for_future_sessions)
def enable_for_this_session(self):
if not SafeViewer.enabled:
for instance in SafeViewer.instances:
instance.enable()
"""FreeCAD uses a modified version of QuarterWidget, so the import pivy.quarter one will cause segfaults.
FreeCAD's FreeCADGui.createViewer() puts the viewer widget inside an MDI window, and detaching it without causing segfaults on exit is tricky.
This class contains some kludges to extract the viewer as a standalone widget and destroy it safely."""
def enable_for_future_sessions(self):
if not SafeViewer.enabled:
# Store in prefs
FreeCAD.ParamGet('User parameter:BaseApp/Preferences/Mod/SearchBar').SetBool('PreviewEnabled', True)
# Then enable as usual
self.enable_for_this_session()
enabled = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/SearchBar").GetBool("PreviewEnabled", False)
instances = []
def enable(self):
if not self.instance_enabled:
import FreeCADGui
# TODO: use a mutex wrapping the entire method, if possible
SafeViewer.enabled = True
self.instance_enabled = True # Has this specific instance been enabled?
def __init__(self, parent=None):
super(SafeViewer, self).__init__()
SafeViewer.instances.append(self)
self.init_parent = parent
self.instance_enabled = False # Has this specific instance been enabled?
if SafeViewer.enabled:
self.displaying_warning = False
self.enable()
else:
import FreeCADGui
from PySide6 import QtCore
if (self.displaying_warning):
self.layout().removeWidget(self.lbl_warning)
self.layout().removeWidget(self.btn_enable_for_this_session)
self.layout().removeWidget(self.btn_enable_for_future_sessions)
self.displaying_warning = True
self.lbl_warning = QtGui.QTextEdit()
self.lbl_warning.setReadOnly(True)
self.lbl_warning.setAlignment(QtCore.Qt.AlignTop)
self.lbl_warning.setText(
"Warning: the 3D preview has some stability issues. It can cause FreeCAD to crash (usually when quitting the application) and could in theory cause data loss, inside and outside of FreeCAD."
)
self.btn_enable_for_this_session = QtGui.QPushButton("Enable 3D preview for this session")
self.btn_enable_for_this_session.clicked.connect(self.enable_for_this_session)
self.btn_enable_for_future_sessions = QtGui.QPushButton("Enable 3D preview for future sessions")
self.btn_enable_for_future_sessions.clicked.connect(self.enable_for_future_sessions)
self.setLayout(QtGui.QVBoxLayout())
self.layout().addWidget(self.lbl_warning)
self.layout().addWidget(self.btn_enable_for_this_session)
self.layout().addWidget(self.btn_enable_for_future_sessions)
self.viewer = FreeCADGui.createViewer()
self.graphicsView = self.viewer.graphicsView()
self.oldGraphicsViewParent = self.graphicsView.parent()
self.oldGraphicsViewParentParent = self.oldGraphicsViewParent.parent()
self.oldGraphicsViewParentParentParent = self.oldGraphicsViewParentParent.parent()
def enable_for_this_session(self):
if not SafeViewer.enabled:
for instance in SafeViewer.instances:
instance.enable()
# Avoid segfault but still hide the undesired window by moving it to a new hidden MDI area.
self.hiddenQMDIArea = QtGui.QMdiArea()
self.hiddenQMDIArea.addSubWindow(self.oldGraphicsViewParentParentParent)
def enable_for_future_sessions(self):
if not SafeViewer.enabled:
# Store in prefs
FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/SearchBar").SetBool("PreviewEnabled", True)
# Then enable as usual
self.enable_for_this_session()
self.private_widget = self.oldGraphicsViewParent
self.private_widget.setParent(self.init_parent)
def enable(self):
if not self.instance_enabled:
import FreeCADGui
self.setLayout(QtGui.QVBoxLayout())
self.layout().addWidget(self.private_widget)
self.layout().setContentsMargins(0,0,0,0)
# TODO: use a mutex wrapping the entire method, if possible
SafeViewer.enabled = True
self.instance_enabled = True # Has this specific instance been enabled?
def fin(slf):
slf.finalizer()
if self.displaying_warning:
self.layout().removeWidget(self.lbl_warning)
self.layout().removeWidget(self.btn_enable_for_this_session)
self.layout().removeWidget(self.btn_enable_for_future_sessions)
import weakref
weakref.finalize(self, fin, self)
self.viewer = FreeCADGui.createViewer()
self.graphicsView = self.viewer.graphicsView()
self.oldGraphicsViewParent = self.graphicsView.parent()
self.oldGraphicsViewParentParent = self.oldGraphicsViewParent.parent()
self.oldGraphicsViewParentParentParent = self.oldGraphicsViewParentParent.parent()
self.destroyed.connect(self.finalizer)
# Avoid segfault but still hide the undesired window by moving it to a new hidden MDI area.
self.hiddenQMDIArea = QtGui.QMdiArea()
self.hiddenQMDIArea.addSubWindow(self.oldGraphicsViewParentParentParent)
def finalizer(self):
# Cleanup in an order that doesn't cause a segfault:
if SafeViewer.enabled:
self.private_widget.setParent(self.oldGraphicsViewParentParent)
self.oldGraphicsViewParentParentParent.close()
self.oldGraphicsViewParentParentParent = None
self.oldGraphicsViewParentParent = None
self.oldGraphicsViewParent = None
self.graphicsView = None
self.viewer = None
#self.parent = None
self.init_parent = None
self.hiddenQMDIArea = None
self.private_widget = self.oldGraphicsViewParent
self.private_widget.setParent(self.init_parent)
self.setLayout(QtGui.QVBoxLayout())
self.layout().addWidget(self.private_widget)
self.layout().setContentsMargins(0, 0, 0, 0)
def fin(slf):
slf.finalizer()
import weakref
weakref.finalize(self, fin, self)
self.destroyed.connect(self.finalizer)
def finalizer(self):
# Cleanup in an order that doesn't cause a segfault:
if SafeViewer.enabled:
self.private_widget.setParent(self.oldGraphicsViewParentParent)
self.oldGraphicsViewParentParentParent.close()
self.oldGraphicsViewParentParentParent = None
self.oldGraphicsViewParentParent = None
self.oldGraphicsViewParent = None
self.graphicsView = None
self.viewer = None
# self.parent = None
self.init_parent = None
self.hiddenQMDIArea = None
def showSceneGraph(self, g):
import FreeCAD as App
if SafeViewer.enabled:
self.viewer.getViewer().setSceneGraph(g)
self.viewer.setCameraOrientation(App.Rotation(1, 1, 0, 0.2))
self.viewer.fitAll()
def showSceneGraph(self, g):
import FreeCAD as App
if SafeViewer.enabled:
self.viewer.getViewer().setSceneGraph(g)
self.viewer.setCameraOrientation(App.Rotation(1,1,0, 0.2))
self.viewer.fitAll()
"""
# Example use:
from PySide import QtGui
from PySide6 import QtGui
import pivy
from SafeViewer import SafeViewer
sv = SafeViewer()

View File

@ -1,35 +1,38 @@
import os
from PySide import QtGui
from PySide import QtCore
import FreeCADGui # just used for FreeCADGui.updateGui()
from PySide6 import QtGui
from PySide6 import QtCore
import FreeCADGui # just used for FreeCADGui.updateGui()
from SearchBoxLight import SearchBoxLight
globalIgnoreFocusOut = False
genericToolIcon = QtGui.QIcon(QtGui.QIcon(os.path.dirname(__file__) + '/Tango-Tools-spanner-hammer.svg'))
genericToolIcon = QtGui.QIcon(QtGui.QIcon(os.path.dirname(__file__) + "/Tango-Tools-spanner-hammer.svg"))
def easyToolTipWidget(html):
foo = QtGui.QTextEdit()
foo.setReadOnly(True)
foo.setAlignment(QtCore.Qt.AlignTop)
foo.setText(html)
return foo
foo = QtGui.QTextEdit()
foo.setReadOnly(True)
foo.setAlignment(QtCore.Qt.AlignTop)
foo.setText(html)
return foo
class SearchBox(QtGui.QLineEdit):
# The following block of code is present in the lightweight proxy SearchBoxLight
'''
resultSelected = QtCore.Signal(int, int)
'''
@staticmethod
def lazyInit(self):
if self.isInitialized:
return self
getItemGroups = self.getItemGroups
getToolTip = self.getToolTip
getItemDelegate = self.getItemDelegate
maxVisibleRows = self.maxVisibleRows
# The following block of code is executed by the lightweight proxy SearchBoxLight
'''
# The following block of code is present in the lightweight proxy SearchBoxLight
"""
resultSelected = QtCore.Signal(int, int)
"""
@staticmethod
def lazyInit(self):
if self.isInitialized:
return self
getItemGroups = self.getItemGroups
getToolTip = self.getToolTip
getItemDelegate = self.getItemDelegate
maxVisibleRows = self.maxVisibleRows
# The following block of code is executed by the lightweight proxy SearchBoxLight
"""
# Call parent constructor
super(SearchBoxLight, self).__init__(parent)
# Connect signals and slots
@ -41,332 +44,382 @@ class SearchBox(QtGui.QLineEdit):
self.setClearButtonEnabled(True)
self.setPlaceholderText('Search tools, prefs & tree')
self.setFixedWidth(200) # needed to avoid a change of width when the clear button appears/disappears
'''
"""
# Save arguments
#self.model = model
self.getItemGroups = getItemGroups
self.getToolTip = getToolTip
self.itemGroups = None # Will be initialized by calling getItemGroups() the first time the search box gains focus, through focusInEvent and refreshItemGroups
self.maxVisibleRows = maxVisibleRows # TODO: use this to compute the correct height
# Create proxy model
self.proxyModel = QtCore.QIdentityProxyModel()
# Filtered model to which items are manually added. Store it as a property of the object instead of a local variable, to prevent grbage collection.
self.mdl = QtGui.QStandardItemModel()
#self.proxyModel.setModel(self.model)
# Create list view
self.listView = QtGui.QListView(self)
self.listView.setWindowFlags(QtGui.Qt.ToolTip)
self.listView.setWindowFlag(QtGui.Qt.FramelessWindowHint)
self.listView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
self.listView.setModel(self.proxyModel)
self.listView.setItemDelegate(getItemDelegate()) # https://stackoverflow.com/a/65930408/324969
# make the QListView non-editable
self.listView.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
# Create pane for showing extra info about the currently-selected tool
#self.extraInfo = QtGui.QLabel()
self.extraInfo = QtGui.QWidget()
self.extraInfo.setWindowFlags(QtGui.Qt.ToolTip)
self.extraInfo.setWindowFlag(QtGui.Qt.FramelessWindowHint)
self.extraInfo.setLayout(QtGui.QVBoxLayout())
self.extraInfo.layout().setContentsMargins(0,0,0,0)
self.setExtraInfoIsActive = False
self.pendingExtraInfo = None
self.currentExtraInfo = None
# Connect signals and slots
self.listView.clicked.connect(lambda x: self.selectResult('select', x))
self.listView.selectionModel().selectionChanged.connect(self.onSelectionChanged)
# Save arguments
# self.model = model
self.getItemGroups = getItemGroups
self.getToolTip = getToolTip
self.itemGroups = None # Will be initialized by calling getItemGroups() the first time the search box gains focus, through focusInEvent and refreshItemGroups
self.maxVisibleRows = maxVisibleRows # TODO: use this to compute the correct height
# Create proxy model
self.proxyModel = QtCore.QIdentityProxyModel()
# Filtered model to which items are manually added. Store it as a property of the object instead of a local variable, to prevent grbage collection.
self.mdl = QtGui.QStandardItemModel()
# self.proxyModel.setModel(self.model)
# Create list view
self.listView = QtGui.QListView(self)
self.listView.setWindowFlags(QtGui.Qt.ToolTip)
self.listView.setWindowFlag(QtGui.Qt.FramelessWindowHint)
self.listView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
self.listView.setModel(self.proxyModel)
self.listView.setItemDelegate(getItemDelegate()) # https://stackoverflow.com/a/65930408/324969
# make the QListView non-editable
self.listView.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
# Create pane for showing extra info about the currently-selected tool
# self.extraInfo = QtGui.QLabel()
self.extraInfo = QtGui.QWidget()
self.extraInfo.setWindowFlags(QtGui.Qt.ToolTip)
self.extraInfo.setWindowFlag(QtGui.Qt.FramelessWindowHint)
self.extraInfo.setLayout(QtGui.QVBoxLayout())
self.extraInfo.layout().setContentsMargins(0, 0, 0, 0)
self.setExtraInfoIsActive = False
self.pendingExtraInfo = None
self.currentExtraInfo = None
# Connect signals and slots
self.listView.clicked.connect(lambda x: self.selectResult("select", x))
self.listView.selectionModel().selectionChanged.connect(self.onSelectionChanged)
# Note: should probably use the eventFilter method instead...
wdgctx = QtCore.Qt.ShortcutContext.WidgetShortcut
# Note: should probably use the eventFilter method instead...
wdgctx = QtCore.Qt.ShortcutContext.WidgetShortcut
QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Down), self, context = wdgctx).activated.connect(self.listDown)
QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Up), self, context = wdgctx).activated.connect(self.listUp)
QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_PageDown), self, context = wdgctx).activated.connect(self.listPageDown)
QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_PageUp), self, context = wdgctx).activated.connect(self.listPageUp)
QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Down), self, context=wdgctx).activated.connect(self.listDown)
QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Up), self, context=wdgctx).activated.connect(self.listUp)
QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_PageDown), self, context=wdgctx).activated.connect(
self.listPageDown
)
QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_PageUp), self, context=wdgctx).activated.connect(
self.listPageUp
)
# Home and End do not work, for some reason.
#QtGui.QShortcut(QtGui.QKeySequence.MoveToEndOfDocument, self, context = wdgctx).activated.connect(self.listEnd)
#QtGui.QShortcut(QtGui.QKeySequence.MoveToStartOfDocument, self, context = wdgctx).activated.connect(self.listStart)
#QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_End), self, context = wdgctx).activated.connect(self.listEnd)
#QtGui.QShortcut(QtGui.QKeySequence('Home'), self, context = wdgctx).activated.connect(self.listStart)
QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Enter), self, context = wdgctx).activated.connect(self.listAccept)
QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Return), self, context = wdgctx).activated.connect(self.listAccept)
QtGui.QShortcut(QtGui.QKeySequence('Ctrl+Return'), self, context = wdgctx).activated.connect(self.listAcceptToggle)
QtGui.QShortcut(QtGui.QKeySequence('Ctrl+Enter'), self, context = wdgctx).activated.connect(self.listAcceptToggle)
QtGui.QShortcut(QtGui.QKeySequence('Ctrl+Space'), self, context = wdgctx).activated.connect(self.listAcceptToggle)
QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Escape), self, context = wdgctx).activated.connect(self.listCancel)
# Home and End do not work, for some reason.
# QtGui.QShortcut(QtGui.QKeySequence.MoveToEndOfDocument, self, context = wdgctx).activated.connect(self.listEnd)
# QtGui.QShortcut(QtGui.QKeySequence.MoveToStartOfDocument, self, context = wdgctx).activated.connect(self.listStart)
# QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_End), self, context = wdgctx).activated.connect(self.listEnd)
# QtGui.QShortcut(QtGui.QKeySequence('Home'), self, context = wdgctx).activated.connect(self.listStart)
# Initialize the model with the full list (assuming the text() is empty)
#self.proxyFilterModel(self.text()) # This is done by refreshItemGroups on focusInEvent, because the initial loading from cache can take time
self.firstShowList = True
self.isInitialized = True
return self
QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Enter), self, context=wdgctx).activated.connect(
self.listAccept
)
QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Return), self, context=wdgctx).activated.connect(
self.listAccept
)
QtGui.QShortcut(QtGui.QKeySequence("Ctrl+Return"), self, context=wdgctx).activated.connect(
self.listAcceptToggle
)
QtGui.QShortcut(QtGui.QKeySequence("Ctrl+Enter"), self, context=wdgctx).activated.connect(self.listAcceptToggle)
QtGui.QShortcut(QtGui.QKeySequence("Ctrl+Space"), self, context=wdgctx).activated.connect(self.listAcceptToggle)
@staticmethod
def refreshItemGroups(self):
self.itemGroups = self.getItemGroups()
self.proxyFilterModel(self.text())
QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Escape), self, context=wdgctx).activated.connect(
self.listCancel
)
@staticmethod
def proxyFocusInEvent(self, qFocusEvent):
if self.firstShowList:
mdl = QtGui.QStandardItemModel()
mdl.appendRow([QtGui.QStandardItem(genericToolIcon, 'Please wait, loading results from cache…'),
QtGui.QStandardItem('0'),
QtGui.QStandardItem('-1')])
self.proxyModel.setSourceModel(mdl)
self.showList()
self.firstShowList = False
FreeCADGui.updateGui()
global globalIgnoreFocusOut
if not globalIgnoreFocusOut:
self.refreshItemGroups()
self.showList()
super(SearchBoxLight, self).focusInEvent(qFocusEvent)
# Initialize the model with the full list (assuming the text() is empty)
# self.proxyFilterModel(self.text()) # This is done by refreshItemGroups on focusInEvent, because the initial loading from cache can take time
self.firstShowList = True
self.isInitialized = True
return self
@staticmethod
def proxyFocusOutEvent(self, qFocusEvent):
global globalIgnoreFocusOut
if not globalIgnoreFocusOut:
self.hideList()
super(SearchBoxLight, self).focusOutEvent(qFocusEvent)
@staticmethod
def refreshItemGroups(self):
self.itemGroups = self.getItemGroups()
self.proxyFilterModel(self.text())
@staticmethod
def movementKey(self, rowUpdate):
currentIndex = self.listView.currentIndex()
self.showList()
if self.listView.isEnabled():
currentRow = currentIndex.row()
@staticmethod
def proxyFocusInEvent(self, qFocusEvent):
if self.firstShowList:
mdl = QtGui.QStandardItemModel()
mdl.appendRow(
[
QtGui.QStandardItem(genericToolIcon, "Please wait, loading results from cache…"),
QtGui.QStandardItem("0"),
QtGui.QStandardItem("-1"),
]
)
self.proxyModel.setSourceModel(mdl)
self.showList()
self.firstShowList = False
FreeCADGui.updateGui()
global globalIgnoreFocusOut
if not globalIgnoreFocusOut:
self.refreshItemGroups()
self.showList()
super(SearchBoxLight, self).focusInEvent(qFocusEvent)
@staticmethod
def proxyFocusOutEvent(self, qFocusEvent):
global globalIgnoreFocusOut
if not globalIgnoreFocusOut:
self.hideList()
super(SearchBoxLight, self).focusOutEvent(qFocusEvent)
@staticmethod
def movementKey(self, rowUpdate):
currentIndex = self.listView.currentIndex()
self.showList()
if self.listView.isEnabled():
currentRow = currentIndex.row()
nbRows = self.listView.model().rowCount()
if nbRows > 0:
newRow = rowUpdate(currentRow, nbRows)
index = self.listView.model().index(newRow, 0)
self.listView.setCurrentIndex(index)
@staticmethod
def proxyListDown(self):
self.movementKey(lambda current, nbRows: (current + 1) % nbRows)
@staticmethod
def proxyListUp(self):
self.movementKey(lambda current, nbRows: (current - 1) % nbRows)
@staticmethod
def proxyListPageDown(self):
self.movementKey(lambda current, nbRows: min(current + max(1, self.maxVisibleRows / 2), nbRows - 1))
@staticmethod
def proxyListPageUp(self):
self.movementKey(lambda current, nbRows: max(current - max(1, self.maxVisibleRows / 2), 0))
@staticmethod
def proxyListEnd(self):
self.movementKey(lambda current, nbRows: nbRows - 1)
@staticmethod
def proxyListStart(self):
self.movementKey(lambda current, nbRows: 0)
@staticmethod
def acceptKey(self, mode):
currentIndex = self.listView.currentIndex()
self.showList()
if currentIndex.isValid():
self.selectResult(mode, currentIndex)
@staticmethod
def proxyListAccept(self):
self.acceptKey("select")
@staticmethod
def proxyListAcceptToggle(self):
self.acceptKey("toggle")
@staticmethod
def cancelKey(self):
self.hideList()
self.clearFocus()
# QKeySequence::Cancel
@staticmethod
def proxyListCancel(self):
self.cancelKey()
@staticmethod
def proxyKeyPressEvent(self, qKeyEvent):
key = qKeyEvent.key()
modifiers = qKeyEvent.modifiers()
self.showList()
if key == QtCore.Qt.Key_Home and modifiers & QtCore.Qt.CTRL != 0:
self.listStart()
elif key == QtCore.Qt.Key_End and modifiers & QtCore.Qt.CTRL != 0:
self.listEnd()
else:
super(SearchBoxLight, self).keyPressEvent(qKeyEvent)
@staticmethod
def showList(self):
self.setFloatingWidgetsGeometry()
if not self.listView.isVisible():
self.listView.show()
self.showExtraInfo()
@staticmethod
def hideList(self):
self.listView.hide()
self.hideExtraInfo()
@staticmethod
def hideExtraInfo(self):
self.extraInfo.hide()
@staticmethod
def selectResult(self, mode, index):
groupId = int(index.model().itemData(index.siblingAtColumn(2))[0])
self.hideList()
# TODO: allow other options, e.g. some items could act as combinators / cumulative filters
self.setText("")
self.proxyFilterModel(self.text())
# TODO: emit index relative to the base model
self.resultSelected.emit(index, groupId)
@staticmethod
def proxyFilterModel(self, userInput):
# TODO: this will cause a race condition if it is accessed while being modified
def matches(s):
return userInput.lower() in s.lower()
def filterGroup(group):
if matches(group["text"]):
# If a group matches, include the entire subtree (might need to disable this if it causes too much noise)
return group
else:
subitems = filterGroups(group["subitems"])
if len(subitems) > 0 or matches(group["text"]):
return {
"id": group["id"],
"text": group["text"],
"icon": group["icon"],
"action": group["action"],
"toolTip": group["toolTip"],
"subitems": subitems,
}
else:
return None
def filterGroups(groups):
groups = (filterGroup(group) for group in groups)
return [group for group in groups if group is not None]
self.mdl = QtGui.QStandardItemModel()
self.mdl.appendColumn([])
def addGroups(filteredGroups, depth=0):
for group in filteredGroups:
# TODO: this is not very clean, we should memorize the index from the original itemgroups
self.mdl.appendRow(
[
QtGui.QStandardItem(group["icon"] or genericToolIcon, group["text"]),
QtGui.QStandardItem(str(depth)),
QtGui.QStandardItem(str(group["id"])),
]
)
addGroups(group["subitems"], depth + 1)
addGroups(filterGroups(self.itemGroups))
self.proxyModel.setSourceModel(self.mdl)
self.currentExtraInfo = None # Unset this so that the ExtraInfo can be updated
# TODO: try to find the already-highlighted item
nbRows = self.listView.model().rowCount()
if nbRows > 0:
newRow = rowUpdate(currentRow, nbRows)
index = self.listView.model().index(newRow, 0)
self.listView.setCurrentIndex(index)
@staticmethod
def proxyListDown(self): self.movementKey(lambda current, nbRows: (current + 1) % nbRows)
@staticmethod
def proxyListUp(self): self.movementKey(lambda current, nbRows: (current - 1) % nbRows)
@staticmethod
def proxyListPageDown(self): self.movementKey(lambda current, nbRows: min(current + max(1, self.maxVisibleRows / 2), nbRows - 1))
@staticmethod
def proxyListPageUp(self): self.movementKey(lambda current, nbRows: max(current - max(1, self.maxVisibleRows / 2), 0))
@staticmethod
def proxyListEnd(self): self.movementKey(lambda current, nbRows: nbRows - 1)
@staticmethod
def proxyListStart(self): self.movementKey(lambda current, nbRows: 0)
@staticmethod
def acceptKey(self, mode):
currentIndex = self.listView.currentIndex()
self.showList()
if currentIndex.isValid():
self.selectResult(mode, currentIndex)
@staticmethod
def proxyListAccept(self): self.acceptKey('select')
@staticmethod
def proxyListAcceptToggle(self): self.acceptKey('toggle')
@staticmethod
def cancelKey(self):
self.hideList()
self.clearFocus()
# QKeySequence::Cancel
@staticmethod
def proxyListCancel(self): self.cancelKey()
@staticmethod
def proxyKeyPressEvent(self, qKeyEvent):
key = qKeyEvent.key()
modifiers = qKeyEvent.modifiers()
self.showList()
if key == QtCore.Qt.Key_Home and modifiers & QtCore.Qt.CTRL != 0:
self.listStart()
elif key == QtCore.Qt.Key_End and modifiers & QtCore.Qt.CTRL != 0:
self.listEnd()
else:
super(SearchBoxLight, self).keyPressEvent(qKeyEvent)
@staticmethod
def showList(self):
self.setFloatingWidgetsGeometry()
if not self.listView.isVisible():
self.listView.show()
self.showExtraInfo()
@staticmethod
def hideList(self):
self.listView.hide()
self.hideExtraInfo()
@staticmethod
def hideExtraInfo(self):
self.extraInfo.hide()
@staticmethod
def selectResult(self, mode, index):
groupId = int(index.model().itemData(index.siblingAtColumn(2))[0])
self.hideList()
# TODO: allow other options, e.g. some items could act as combinators / cumulative filters
self.setText('')
self.proxyFilterModel(self.text())
# TODO: emit index relative to the base model
self.resultSelected.emit(index, groupId)
@staticmethod
def proxyFilterModel(self, userInput):
# TODO: this will cause a race condition if it is accessed while being modified
def matches(s):
return userInput.lower() in s.lower()
def filterGroup(group):
if matches(group['text']):
# If a group matches, include the entire subtree (might need to disable this if it causes too much noise)
return group
else:
subitems = filterGroups(group['subitems'])
if len(subitems) > 0 or matches(group['text']):
return { 'id': group['id'], 'text': group['text'], 'icon': group['icon'], 'action': group['action'], 'toolTip':group['toolTip'], 'subitems': subitems }
index = self.listView.model().index(0, 0)
self.listView.setCurrentIndex(index)
self.setExtraInfo(index)
else:
return None
def filterGroups(groups):
groups = (filterGroup(group) for group in groups)
return [group for group in groups if group is not None]
self.mdl = QtGui.QStandardItemModel()
self.mdl.appendColumn([])
def addGroups(filteredGroups, depth=0):
for group in filteredGroups:
# TODO: this is not very clean, we should memorize the index from the original itemgroups
self.mdl.appendRow([QtGui.QStandardItem(group['icon'] or genericToolIcon, group['text']),
QtGui.QStandardItem(str(depth)),
QtGui.QStandardItem(str(group['id']))])
addGroups(group['subitems'], depth+1)
addGroups(filterGroups(self.itemGroups))
self.proxyModel.setSourceModel(self.mdl)
self.currentExtraInfo = None # Unset this so that the ExtraInfo can be updated
# TODO: try to find the already-highlighted item
nbRows = self.listView.model().rowCount()
if nbRows > 0:
index = self.listView.model().index(0, 0)
self.listView.setCurrentIndex(index)
self.setExtraInfo(index)
else:
self.clearExtraInfo()
#self.showList()
self.clearExtraInfo()
# self.showList()
@staticmethod
def setFloatingWidgetsGeometry(self):
def getScreenPosition(widget):
geo = widget.geometry()
parent = widget.parent()
parentPos = getScreenPosition(parent) if parent is not None else QtCore.QPoint(0,0)
return QtCore.QPoint(geo.x() + parentPos.x(), geo.y() + parentPos.y())
pos = getScreenPosition(self)
siz = self.size()
screen = QtGui.QGuiApplication.screenAt(pos)
x = pos.x()
y = pos.y() + siz.height()
hint_w = self.listView.sizeHint().width()
# TODO: this can still bump into the bottom of the screen, in that case we should flip
w = max(siz.width(), hint_w)
h = 200 # TODO: set height / size here according to desired number of items
extraw = w # choose a preferred width that doesn't change all the time,
# self.extraInfo.sizeHint().width() would change for every item.
extrax = x - extraw
if screen is not None:
scr = screen.geometry()
x = min(scr.x() + scr.width() - hint_w, x)
extraleftw = x - scr.x()
extrarightw = scr.x() + scr.width() - x
# flip the extraInfo if it doesn't fit on the screen
if extraleftw < extraw and extrarightw > extraleftw:
extrax = x + w
extraw = min(extrarightw, extraw)
else:
@staticmethod
def setFloatingWidgetsGeometry(self):
def getScreenPosition(widget):
geo = widget.geometry()
parent = widget.parent()
parentPos = getScreenPosition(parent) if parent is not None else QtCore.QPoint(0, 0)
return QtCore.QPoint(geo.x() + parentPos.x(), geo.y() + parentPos.y())
pos = getScreenPosition(self)
siz = self.size()
screen = QtGui.QGuiApplication.screenAt(pos)
x = pos.x()
y = pos.y() + siz.height()
hint_w = self.listView.sizeHint().width()
# TODO: this can still bump into the bottom of the screen, in that case we should flip
w = max(siz.width(), hint_w)
h = 200 # TODO: set height / size here according to desired number of items
extraw = w # choose a preferred width that doesn't change all the time,
# self.extraInfo.sizeHint().width() would change for every item.
extrax = x - extraw
extraw = min(extraleftw, extraw)
self.listView.setGeometry(x, y, w, h)
self.extraInfo.setGeometry(extrax, y, extraw, h)
if screen is not None:
scr = screen.geometry()
x = min(scr.x() + scr.width() - hint_w, x)
extraleftw = x - scr.x()
extrarightw = scr.x() + scr.width() - x
# flip the extraInfo if it doesn't fit on the screen
if extraleftw < extraw and extrarightw > extraleftw:
extrax = x + w
extraw = min(extrarightw, extraw)
else:
extrax = x - extraw
extraw = min(extraleftw, extraw)
self.listView.setGeometry(x, y, w, h)
self.extraInfo.setGeometry(extrax, y, extraw, h)
@staticmethod
def proxyOnSelectionChanged(self, selected, deselected):
# The list has .setSelectionMode(QtGui.QAbstractItemView.SingleSelection),
# so there is always at most one index in selected.indexes() and at most one
# index in deselected.indexes()
selected = selected.indexes()
deselected = deselected.indexes()
if len(selected) > 0:
index = selected[0]
self.setExtraInfo(index)
# Poor attempt to circumvent a glitch where the extra info pane stays visible after pressing Return
if not self.listView.isHidden():
self.showExtraInfo()
elif len(deselected) > 0:
self.hideExtraInfo()
@staticmethod
def proxyOnSelectionChanged(self, selected, deselected):
# The list has .setSelectionMode(QtGui.QAbstractItemView.SingleSelection),
# so there is always at most one index in selected.indexes() and at most one
# index in deselected.indexes()
selected = selected.indexes()
deselected = deselected.indexes()
if len(selected) > 0:
index = selected[0]
self.setExtraInfo(index)
# Poor attempt to circumvent a glitch where the extra info pane stays visible after pressing Return
if not self.listView.isHidden():
self.showExtraInfo()
elif len(deselected) > 0:
self.hideExtraInfo()
@staticmethod
def setExtraInfo(self, index):
if self.currentExtraInfo == (index.row(), index.column(), index.model()):
# avoid useless updates of the extra info window; this also prevents segfaults when the widget
# is replaced when selecting an option from the right-click context menu
return
self.currentExtraInfo = (index.row(), index.column(), index.model())
# TODO: use an atomic swap or mutex if possible
if self.setExtraInfoIsActive:
self.pendingExtraInfo = index
#print("boom")
else:
self.setExtraInfoIsActive = True
#print("lock")
# setExtraInfo can be called multiple times while this function is running,
# so just before existing we check for the latest pending call and execute it,
# if during that second execution some other calls are made the latest of those will
# be queued by the code a few lines above this one, and the loop will continue processing
# until an iteration during which no further call was made.
while True:
groupId = str(index.model().itemData(index.siblingAtColumn(2))[0])
# TODO: move this outside of this class, probably use a single metadata
# This is a hack to allow some widgets to set the parent and recompute their size
# during their construction.
parentIsSet = False
def setParent(toolTipWidget):
nonlocal parentIsSet
parentIsSet = True
w = self.extraInfo.layout().takeAt(0)
while w:
if hasattr(w.widget(), 'finalizer'):
# The 3D viewer segfaults very easily if it is used after being destroyed, and some Python/C++ interop seems to overzealously destroys some widgets, including this one, too soon?
# Ensuring that we properly detacth the 3D viewer widget before discarding its parent seems to avoid these crashes.
#print('FINALIZER')
w.widget().finalizer()
if w.widget() is not None:
w.widget().hide() # hide before detaching, or we have widgets floating as their own window that appear for a split second in some cases.
w.widget().setParent(None)
w = self.extraInfo.layout().takeAt(0)
self.extraInfo.layout().addWidget(toolTipWidget)
self.setFloatingWidgetsGeometry()
toolTipWidget = self.getToolTip(groupId, setParent)
if isinstance(toolTipWidget, str):
toolTipWidget = easyToolTipWidget(toolTipWidget)
if not parentIsSet:
setParent(toolTipWidget)
if self.pendingExtraInfo is not None:
index = self.pendingExtraInfo
self.pendingExtraInfo = None
@staticmethod
def setExtraInfo(self, index):
if self.currentExtraInfo == (index.row(), index.column(), index.model()):
# avoid useless updates of the extra info window; this also prevents segfaults when the widget
# is replaced when selecting an option from the right-click context menu
return
self.currentExtraInfo = (index.row(), index.column(), index.model())
# TODO: use an atomic swap or mutex if possible
if self.setExtraInfoIsActive:
self.pendingExtraInfo = index
# print("boom")
else:
break
#print("unlock")
self.setExtraInfoIsActive = False
self.setExtraInfoIsActive = True
# print("lock")
# setExtraInfo can be called multiple times while this function is running,
# so just before existing we check for the latest pending call and execute it,
# if during that second execution some other calls are made the latest of those will
# be queued by the code a few lines above this one, and the loop will continue processing
# until an iteration during which no further call was made.
while True:
groupId = str(index.model().itemData(index.siblingAtColumn(2))[0])
# TODO: move this outside of this class, probably use a single metadata
# This is a hack to allow some widgets to set the parent and recompute their size
# during their construction.
parentIsSet = False
@staticmethod
def clearExtraInfo(self):
# TODO: just clear the contents but keep the widget visible.
self.extraInfo.hide()
def setParent(toolTipWidget):
nonlocal parentIsSet
parentIsSet = True
w = self.extraInfo.layout().takeAt(0)
while w:
if hasattr(w.widget(), "finalizer"):
# The 3D viewer segfaults very easily if it is used after being destroyed, and some Python/C++ interop seems to overzealously destroys some widgets, including this one, too soon?
# Ensuring that we properly detacth the 3D viewer widget before discarding its parent seems to avoid these crashes.
# print('FINALIZER')
w.widget().finalizer()
if w.widget() is not None:
w.widget().hide() # hide before detaching, or we have widgets floating as their own window that appear for a split second in some cases.
w.widget().setParent(None)
w = self.extraInfo.layout().takeAt(0)
self.extraInfo.layout().addWidget(toolTipWidget)
self.setFloatingWidgetsGeometry()
@staticmethod
def showExtraInfo(self):
self.extraInfo.show()
toolTipWidget = self.getToolTip(groupId, setParent)
if isinstance(toolTipWidget, str):
toolTipWidget = easyToolTipWidget(toolTipWidget)
if not parentIsSet:
setParent(toolTipWidget)
if self.pendingExtraInfo is not None:
index = self.pendingExtraInfo
self.pendingExtraInfo = None
else:
break
# print("unlock")
self.setExtraInfoIsActive = False
@staticmethod
def clearExtraInfo(self):
# TODO: just clear the contents but keep the widget visible.
self.extraInfo.hide()
@staticmethod
def showExtraInfo(self):
self.extraInfo.show()

View File

@ -1,49 +1,84 @@
from PySide import QtGui
from PySide import QtCore
from PySide6 import QtGui
from PySide6 import QtCore
# This is a "light" version of the SearchBox implementation, which loads the actual implementation on first click
class SearchBoxLight(QtGui.QLineEdit):
resultSelected = QtCore.Signal(int, int)
def __init__(self, getItemGroups, getToolTip, getItemDelegate, maxVisibleRows = 20, parent = None):
self.isInitialized = False
resultSelected = QtCore.Signal(int, int)
# Store arguments
self.getItemGroups = getItemGroups
self.getToolTip = getToolTip
self.getItemDelegate = getItemDelegate
self.maxVisibleRows = maxVisibleRows
def __init__(self, getItemGroups, getToolTip, getItemDelegate, maxVisibleRows=20, parent=None):
self.isInitialized = False
# Call parent constructor
super(SearchBoxLight, self).__init__(parent)
# Connect signals and slots
self.textChanged.connect(self.filterModel)
# Thanks to https://saurabhg.com/programming/search-box-using-qlineedit/ for indicating a few useful options
ico = QtGui.QIcon(':/icons/help-browser.svg')
#ico = QtGui.QIcon(':/icons/WhatsThis.svg')
self.addAction(ico, QtGui.QLineEdit.LeadingPosition)
self.setClearButtonEnabled(True)
self.setPlaceholderText('Search tools, prefs & tree')
self.setFixedWidth(200) # needed to avoid a change of width when the clear button appears/disappears
def lazyInit(self):
pass
def __getattr__(self, name):
import types
def f(*args, **kwargs):
import SearchBox
SearchBox.SearchBox.lazyInit(self)
return getattr(SearchBox.SearchBox, name)(*args, **kwargs)
return types.MethodType(f, self)
def focusInEvent(self, *args, **kwargs): return self.proxyFocusInEvent(*args, **kwargs)
def focusOutEvent(self, *args, **kwargs): return self.proxyFocusOutEvent(*args, **kwargs)
def keyPressEvent(self, *args, **kwargs): return self.proxyKeyPressEvent(*args, **kwargs)
def onSelectionChanged(self, *args, **kwargs): return self.proxyOnSelectionChanged(*args, **kwargs)
def filterModel(self, *args, **kwargs): return self.proxyFilterModel(*args, **kwargs)
def listDown(self, *args, **kwargs): return self.proxyListDown(*args, **kwargs)
def listUp(self, *args, **kwargs): return self.proxyListUp(*args, **kwargs)
def listPageDown(self, *args, **kwargs): return self.proxyListPageDown(*args, **kwargs)
def listPageUp(self, *args, **kwargs): return self.proxyListPageUp(*args, **kwargs)
def listEnd(self, *args, **kwargs): return self.proxyListEnd(*args, **kwargs)
def listStart(self, *args, **kwargs): return self.proxyListStart(*args, **kwargs)
def listAccept(self, *args, **kwargs): return self.proxyListAccept(*args, **kwargs)
def listAcceptToggle(self, *args, **kwargs): return self.proxyListAcceptToggle(*args, **kwargs)
def listCancel(self, *args, **kwargs): return self.proxyListCancel(*args, **kwargs)
# Store arguments
self.getItemGroups = getItemGroups
self.getToolTip = getToolTip
self.getItemDelegate = getItemDelegate
self.maxVisibleRows = maxVisibleRows
# Call parent constructor
super(SearchBoxLight, self).__init__(parent)
# Connect signals and slots
self.textChanged.connect(self.filterModel)
# Thanks to https://saurabhg.com/programming/search-box-using-qlineedit/ for indicating a few useful options
ico = QtGui.QIcon(":/icons/help-browser.svg")
# ico = QtGui.QIcon(':/icons/WhatsThis.svg')
self.addAction(ico, QtGui.QLineEdit.LeadingPosition)
self.setClearButtonEnabled(True)
self.setPlaceholderText("Search tools, prefs & tree")
self.setFixedWidth(200) # needed to avoid a change of width when the clear button appears/disappears
def lazyInit(self):
pass
def __getattr__(self, name):
import types
def f(*args, **kwargs):
import SearchBox
SearchBox.SearchBox.lazyInit(self)
return getattr(SearchBox.SearchBox, name)(*args, **kwargs)
return types.MethodType(f, self)
def focusInEvent(self, *args, **kwargs):
return self.proxyFocusInEvent(*args, **kwargs)
def focusOutEvent(self, *args, **kwargs):
return self.proxyFocusOutEvent(*args, **kwargs)
def keyPressEvent(self, *args, **kwargs):
return self.proxyKeyPressEvent(*args, **kwargs)
def onSelectionChanged(self, *args, **kwargs):
return self.proxyOnSelectionChanged(*args, **kwargs)
def filterModel(self, *args, **kwargs):
return self.proxyFilterModel(*args, **kwargs)
def listDown(self, *args, **kwargs):
return self.proxyListDown(*args, **kwargs)
def listUp(self, *args, **kwargs):
return self.proxyListUp(*args, **kwargs)
def listPageDown(self, *args, **kwargs):
return self.proxyListPageDown(*args, **kwargs)
def listPageUp(self, *args, **kwargs):
return self.proxyListPageUp(*args, **kwargs)
def listEnd(self, *args, **kwargs):
return self.proxyListEnd(*args, **kwargs)
def listStart(self, *args, **kwargs):
return self.proxyListStart(*args, **kwargs)
def listAccept(self, *args, **kwargs):
return self.proxyListAccept(*args, **kwargs)
def listAcceptToggle(self, *args, **kwargs):
return self.proxyListAcceptToggle(*args, **kwargs)
def listCancel(self, *args, **kwargs):
return self.proxyListCancel(*args, **kwargs)

View File

@ -1,87 +1,130 @@
from PySide import QtCore
from PySide import QtGui
from PySide6 import QtCore
from PySide6 import QtGui
import json
def iconToBase64(icon, sz = QtCore.QSize(64,64), mode = QtGui.QIcon.Mode.Normal, state = QtGui.QIcon.State.On):
buf = QtCore.QBuffer()
buf.open(QtCore.QIODevice.WriteOnly)
icon.pixmap(sz, mode, state).save(buf, 'PNG')
return QtCore.QTextCodec.codecForName('UTF-8').toUnicode(buf.data().toBase64())
def iconToHTML(icon, sz = 12, mode = QtGui.QIcon.Mode.Normal, state = QtGui.QIcon.State.On):
return '<img width="'+str(sz)+'" height="'+str(sz)+'" src="data:image/png;base64,' + iconToBase64(icon, QtCore.QSize(sz,sz), mode, state) + '" />'
def iconToBase64(icon, sz=QtCore.QSize(64, 64), mode=QtGui.QIcon.Mode.Normal, state=QtGui.QIcon.State.On):
buf = QtCore.QBuffer()
buf.open(QtCore.QIODevice.WriteOnly)
icon.pixmap(sz, mode, state).save(buf, "PNG")
result = None
try:
result = QtCore.QTextCodec.codecForName("UTF-8").toUnicode(buf.data().toBase64())
except Exception:
t = QtCore.QTextStream(buf.data().toBase64())
t.setEncoding(QtCore.QStringDecoder.Encoding.Utf8)
result = t.readAll()
return result
def iconToHTML(icon, sz=12, mode=QtGui.QIcon.Mode.Normal, state=QtGui.QIcon.State.On):
return (
'<img width="'
+ str(sz)
+ '" height="'
+ str(sz)
+ '" src="data:image/png;base64,'
+ iconToBase64(icon, QtCore.QSize(sz, sz), mode, state)
+ '" />'
)
def serializeIcon(icon):
iconPixmaps = {}
for sz in icon.availableSizes():
strW = str(sz.width())
strH = str(sz.height())
iconPixmaps[strW] = {}
iconPixmaps[strW][strH] = {}
for strMode, mode in {'normal':QtGui.QIcon.Mode.Normal, 'disabled':QtGui.QIcon.Mode.Disabled, 'active':QtGui.QIcon.Mode.Active, 'selected':QtGui.QIcon.Mode.Selected}.items():
iconPixmaps[strW][strH][strMode] = {}
for strState, state in {'off':QtGui.QIcon.State.Off, 'on':QtGui.QIcon.State.On}.items():
iconPixmaps[strW][strH][strMode][strState] = iconToBase64(icon, sz, mode, state)
return iconPixmaps
iconPixmaps = {}
for sz in icon.availableSizes():
strW = str(sz.width())
strH = str(sz.height())
iconPixmaps[strW] = {}
iconPixmaps[strW][strH] = {}
for strMode, mode in {
"normal": QtGui.QIcon.Mode.Normal,
"disabled": QtGui.QIcon.Mode.Disabled,
"active": QtGui.QIcon.Mode.Active,
"selected": QtGui.QIcon.Mode.Selected,
}.items():
iconPixmaps[strW][strH][strMode] = {}
for strState, state in {"off": QtGui.QIcon.State.Off, "on": QtGui.QIcon.State.On}.items():
iconPixmaps[strW][strH][strMode][strState] = iconToBase64(icon, sz, mode, state)
return iconPixmaps
# workbenches is a list(str), toolbar is a str, text is a str, icon is a QtGui.QIcon
def serializeTool(tool):
return {
'workbenches': tool['workbenches'],
'toolbar': tool['toolbar'],
'text': tool['text'],
'toolTip': tool['toolTip'],
'icon': serializeIcon(tool['icon']),
}
return {
"workbenches": tool["workbenches"],
"toolbar": tool["toolbar"],
"text": tool["text"],
"toolTip": tool["toolTip"],
"icon": serializeIcon(tool["icon"]),
}
def deserializeIcon(iconPixmaps):
ico = QtGui.QIcon()
for strW, wPixmaps in iconPixmaps.items():
for strH, hPixmaps in wPixmaps.items():
for strMode, modePixmaps in hPixmaps.items():
mode = {'normal':QtGui.QIcon.Mode.Normal, 'disabled':QtGui.QIcon.Mode.Disabled, 'active':QtGui.QIcon.Mode.Active, 'selected':QtGui.QIcon.Mode.Selected}[strMode]
for strState, statePixmap in modePixmaps.items():
state = {'off':QtGui.QIcon.State.Off, 'on':QtGui.QIcon.State.On}[strState]
pxm = QtGui.QPixmap()
pxm.loadFromData(QtCore.QByteArray.fromBase64(QtCore.QTextCodec.codecForName('UTF-8').fromUnicode(statePixmap)))
ico.addPixmap(pxm, mode, state)
return ico
ico = QtGui.QIcon()
for strW, wPixmaps in iconPixmaps.items():
for strH, hPixmaps in wPixmaps.items():
for strMode, modePixmaps in hPixmaps.items():
mode = {
"normal": QtGui.QIcon.Mode.Normal,
"disabled": QtGui.QIcon.Mode.Disabled,
"active": QtGui.QIcon.Mode.Active,
"selected": QtGui.QIcon.Mode.Selected,
}[strMode]
for strState, statePixmap in modePixmaps.items():
state = {"off": QtGui.QIcon.State.Off, "on": QtGui.QIcon.State.On}[strState]
pxm = QtGui.QPixmap()
# pxm.loadFromData(
# QtCore.QByteArray.fromBase64(QtCore.QTextCodec.codecForName("UTF-8").fromUnicode(statePixmap))
# )
t = QtCore.QTextStream(statePixmap)
t.setEncoding(QtCore.QStringDecoder.Encoding.Utf8)
pxm.loadFromData(t.readAll())
ico.addPixmap(pxm, mode, state)
return ico
def deserializeTool(tool):
return {
'workbenches': tool['workbenches'],
'toolbar': tool['toolbar'],
'text': tool['text'],
'toolTip': tool['toolTip'],
'icon': deserializeIcon(tool['icon']),
}
return {
"workbenches": tool["workbenches"],
"toolbar": tool["toolbar"],
"text": tool["text"],
"toolTip": tool["toolTip"],
"icon": deserializeIcon(tool["icon"]),
}
def serializeItemGroup(itemGroup):
return {
'icon': serializeIcon(itemGroup['icon']),
'text': itemGroup['text'],
'toolTip': itemGroup['toolTip'],
'action': itemGroup['action'],
'subitems': serializeItemGroups(itemGroup['subitems'])
}
return {
"icon": serializeIcon(itemGroup["icon"]),
"text": itemGroup["text"],
"toolTip": itemGroup["toolTip"],
"action": itemGroup["action"],
"subitems": serializeItemGroups(itemGroup["subitems"]),
}
def serializeItemGroups(itemGroups):
return [serializeItemGroup(itemGroup) for itemGroup in itemGroups]
return [serializeItemGroup(itemGroup) for itemGroup in itemGroups]
def serialize(itemGroups):
return json.dumps(serializeItemGroups(itemGroups))
return json.dumps(serializeItemGroups(itemGroups))
def deserializeItemGroup(itemGroup):
return {
'icon': deserializeIcon(itemGroup['icon']),
'text': itemGroup['text'],
'toolTip': itemGroup['toolTip'],
'action': itemGroup['action'],
'subitems': deserializeItemGroups(itemGroup['subitems'])
}
return {
"icon": deserializeIcon(itemGroup["icon"]),
"text": itemGroup["text"],
"toolTip": itemGroup["toolTip"],
"action": itemGroup["action"],
"subitems": deserializeItemGroups(itemGroup["subitems"]),
}
def deserializeItemGroups(serializedItemGroups):
return [deserializeItemGroup(itemGroup) for itemGroup in serializedItemGroups]
return [deserializeItemGroup(itemGroup) for itemGroup in serializedItemGroups]
def deserialize(serializedItemGroups):
return deserializeItemGroups(json.loads(serializedItemGroups))
return deserializeItemGroups(json.loads(serializedItemGroups))