first test
This commit is contained in:
parent
09b07037a6
commit
bef6f91b3c
105
GetItemGroups.py
105
GetItemGroups.py
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
58
InitGui.py
58
InitGui.py
|
@ -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)
|
||||
|
|
148
RefreshTools.py
148
RefreshTools.py
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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": [],
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
|
188
SafeViewer.py
188
SafeViewer.py
|
@ -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()
|
||||
|
|
719
SearchBox.py
719
SearchBox.py
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
169
Serialize.py
169
Serialize.py
|
@ -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))
|
||||
|
|
Loading…
Reference in New Issue
Block a user