Fixed compatibilty issue with QT6 #36
142
.gitignore
vendored
|
@ -1,2 +1,142 @@
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
.vscode/
|
||||||
__pycache__
|
__pycache__
|
||||||
/gif
|
.pre-commit-config.yaml
|
||||||
|
.pre-commit-search-and-replace.yaml
|
||||||
|
.pttx
|
||||||
|
*.pptx
|
||||||
|
/CreateTranslations.bat
|
||||||
|
*.bak
|
||||||
|
Backups/
|
||||||
|
RibbonDataFile*.dat
|
||||||
|
RibbonStructure.json
|
||||||
|
RibbonStructure_default.json
|
||||||
|
|
16
.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# .pre-commit-config.yaml
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/mattlqx/pre-commit-search-and-replace
|
||||||
|
rev: v1.1.8
|
||||||
|
hooks:
|
||||||
|
- id: search-and-replace
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v5.0.0 # this is optional, use `pre-commit autoupdate` to get the latest rev!
|
||||||
|
hooks:
|
||||||
|
- id: check-yaml
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- repo: https://github.com/psf/black
|
||||||
|
rev: 24.10.0
|
||||||
|
hooks:
|
||||||
|
- id: black
|
6
.pre-commit-search-and-replace.yaml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
- search: PySide6
|
||||||
|
replacement: PySide
|
||||||
|
- search: PySide2
|
||||||
|
replacement: PySide
|
||||||
|
- search: pyqtribbon as
|
||||||
|
replacement: pyqtribbon_local as
|
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"githubPullRequests.ignoredPullRequestBranches": ["main"]
|
||||||
|
}
|
|
@ -3,40 +3,84 @@
|
||||||
|
|
||||||
import SearchResults
|
import SearchResults
|
||||||
|
|
||||||
SearchResults.registerResultProvider('refreshTools',
|
SearchResults.registerResultProvider(
|
||||||
getItemGroupsCached = lambda: __import__('ResultsRefreshTools').refreshToolsResultsProvider(),
|
"refreshTools",
|
||||||
getItemGroupsUncached = lambda: [])
|
getItemGroupsCached=lambda: __import__(
|
||||||
SearchResults.registerResultProvider('document',
|
"ResultsRefreshTools"
|
||||||
getItemGroupsCached = lambda: [],
|
).refreshToolsResultsProvider(),
|
||||||
getItemGroupsUncached = lambda: __import__('ResultsDocument').documentResultsProvider())
|
getItemGroupsUncached=lambda: [],
|
||||||
SearchResults.registerResultProvider('toolbar',
|
)
|
||||||
getItemGroupsCached = lambda: __import__('ResultsToolbar').toolbarResultsProvider(),
|
SearchResults.registerResultProvider(
|
||||||
getItemGroupsUncached = lambda: [])
|
"document",
|
||||||
SearchResults.registerResultProvider('param',
|
getItemGroupsCached=lambda: [],
|
||||||
getItemGroupsCached = lambda: __import__('ResultsPreferences').paramResultsProvider(),
|
getItemGroupsUncached=lambda: __import__(
|
||||||
getItemGroupsUncached = lambda: [])
|
"ResultsDocument"
|
||||||
|
).documentResultsProvider(),
|
||||||
|
)
|
||||||
|
SearchResults.registerResultProvider(
|
||||||
|
"toolbar",
|
||||||
|
getItemGroupsCached=lambda: __import__("ResultsToolbar").toolbarResultsProvider(),
|
||||||
|
getItemGroupsUncached=lambda: [],
|
||||||
|
)
|
||||||
|
SearchResults.registerResultProvider(
|
||||||
|
"param",
|
||||||
|
getItemGroupsCached=lambda: __import__("ResultsPreferences").paramResultsProvider(),
|
||||||
|
getItemGroupsUncached=lambda: [],
|
||||||
|
)
|
||||||
|
|
||||||
SearchResults.registerResultHandler('refreshTools',
|
SearchResults.registerResultHandler(
|
||||||
action = lambda nfo: __import__('ResultsRefreshTools').refreshToolsAction(nfo),
|
"refreshTools",
|
||||||
toolTip = lambda nfo, setParent: __import__('ResultsRefreshTools').refreshToolsToolTip(nfo, setParent))
|
action=lambda nfo: __import__("ResultsRefreshTools").refreshToolsAction(nfo),
|
||||||
SearchResults.registerResultHandler('toolbar',
|
toolTip=lambda nfo, setParent: __import__(
|
||||||
action = lambda nfo: __import__('ResultsToolbar').toolbarAction(nfo),
|
"ResultsRefreshTools"
|
||||||
toolTip = lambda nfo, setParent: __import__('ResultsToolbar').toolbarToolTip(nfo, setParent))
|
).refreshToolsToolTip(nfo, setParent),
|
||||||
SearchResults.registerResultHandler('tool',
|
)
|
||||||
action = lambda nfo : __import__('ResultsToolbar').subToolAction(nfo),
|
SearchResults.registerResultHandler(
|
||||||
toolTip = lambda nfo, setParent: __import__('ResultsToolbar').subToolToolTip(nfo, setParent))
|
"toolbar",
|
||||||
SearchResults.registerResultHandler('subTool',
|
action=lambda nfo: __import__("ResultsToolbar").toolbarAction(nfo),
|
||||||
action = lambda nfo : __import__('ResultsToolbar').subToolAction(nfo),
|
toolTip=lambda nfo, setParent: __import__("ResultsToolbar").toolbarToolTip(
|
||||||
toolTip = lambda nfo, setParent: __import__('ResultsToolbar').subToolToolTip(nfo, setParent))
|
nfo, setParent
|
||||||
SearchResults.registerResultHandler('document',
|
),
|
||||||
action = lambda nfo : __import__('ResultsDocument').documentAction(nfo),
|
)
|
||||||
toolTip = lambda nfo, setParent: __import__('ResultsDocument').documentToolTip(nfo, setParent))
|
SearchResults.registerResultHandler(
|
||||||
SearchResults.registerResultHandler('documentObject',
|
"tool",
|
||||||
action = lambda nfo : __import__('ResultsDocument').documentObjectAction(nfo),
|
action=lambda nfo: __import__("ResultsToolbar").subToolAction(nfo),
|
||||||
toolTip = lambda nfo, setParent: __import__('ResultsDocument').documentObjectToolTip(nfo, setParent))
|
toolTip=lambda nfo, setParent: __import__("ResultsToolbar").subToolToolTip(
|
||||||
SearchResults.registerResultHandler('param',
|
nfo, setParent
|
||||||
action = lambda nfo : __import__('ResultsPreferences').paramAction(nfo),
|
),
|
||||||
toolTip = lambda nfo, setParent: __import__('ResultsPreferences').paramToolTip(nfo, setParent))
|
)
|
||||||
SearchResults.registerResultHandler('paramGroup',
|
SearchResults.registerResultHandler(
|
||||||
action = lambda nfo : __import__('ResultsPreferences').paramGroupAction(nfo),
|
"subTool",
|
||||||
toolTip = lambda nfo, setParent: __import__('ResultsPreferences').paramGroupToolTip(nfo, setParent))
|
action=lambda nfo: __import__("ResultsToolbar").subToolAction(nfo),
|
||||||
|
toolTip=lambda nfo, setParent: __import__("ResultsToolbar").subToolToolTip(
|
||||||
|
nfo, setParent
|
||||||
|
),
|
||||||
|
)
|
||||||
|
SearchResults.registerResultHandler(
|
||||||
|
"document",
|
||||||
|
action=lambda nfo: __import__("ResultsDocument").documentAction(nfo),
|
||||||
|
toolTip=lambda nfo, setParent: __import__("ResultsDocument").documentToolTip(
|
||||||
|
nfo, setParent
|
||||||
|
),
|
||||||
|
)
|
||||||
|
SearchResults.registerResultHandler(
|
||||||
|
"documentObject",
|
||||||
|
action=lambda nfo: __import__("ResultsDocument").documentObjectAction(nfo),
|
||||||
|
toolTip=lambda nfo, setParent: __import__("ResultsDocument").documentObjectToolTip(
|
||||||
|
nfo, setParent
|
||||||
|
),
|
||||||
|
)
|
||||||
|
SearchResults.registerResultHandler(
|
||||||
|
"param",
|
||||||
|
action=lambda nfo: __import__("ResultsPreferences").paramAction(nfo),
|
||||||
|
toolTip=lambda nfo, setParent: __import__("ResultsPreferences").paramToolTip(
|
||||||
|
nfo, setParent
|
||||||
|
),
|
||||||
|
)
|
||||||
|
SearchResults.registerResultHandler(
|
||||||
|
"paramGroup",
|
||||||
|
action=lambda nfo: __import__("ResultsPreferences").paramGroupAction(nfo),
|
||||||
|
toolTip=lambda nfo, setParent: __import__("ResultsPreferences").paramGroupToolTip(
|
||||||
|
nfo, setParent
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
113
GetItemGroups.py
|
@ -1,57 +1,84 @@
|
||||||
|
import FreeCAD as App
|
||||||
|
import FreeCADGui as Gui
|
||||||
|
|
||||||
globalGroups = []
|
globalGroups = []
|
||||||
|
|
||||||
itemGroups = None
|
itemGroups = None
|
||||||
serializedItemGroups = None
|
serializedItemGroups = None
|
||||||
|
|
||||||
|
# Define the translation
|
||||||
|
translate = App.Qt.translate
|
||||||
|
|
||||||
|
|
||||||
def onResultSelected(index, groupId):
|
def onResultSelected(index, groupId):
|
||||||
global globalGroups
|
global globalGroups
|
||||||
nfo = globalGroups[groupId]
|
nfo = globalGroups[groupId]
|
||||||
handlerName = nfo['action']['handler']
|
handlerName = nfo["action"]["handler"]
|
||||||
import SearchResults
|
import SearchResults
|
||||||
if handlerName in SearchResults.actionHandlers:
|
|
||||||
SearchResults.actionHandlers[handlerName](nfo)
|
if handlerName in SearchResults.actionHandlers:
|
||||||
else:
|
SearchResults.actionHandlers[handlerName](nfo)
|
||||||
from PySide import QtGui
|
else:
|
||||||
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.')
|
from PySide import QtGui
|
||||||
|
|
||||||
|
QtGui.QMessageBox.warning(
|
||||||
|
None,
|
||||||
|
translate("SearchBar", "Could not execute this action"),
|
||||||
|
translate(
|
||||||
|
"SearchBar",
|
||||||
|
"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):
|
def getToolTip(groupId, setParent):
|
||||||
global globalGroups
|
global globalGroups
|
||||||
nfo = globalGroups[int(groupId)]
|
nfo = globalGroups[int(groupId)]
|
||||||
handlerName = nfo['action']['handler']
|
handlerName = nfo["action"]["handler"]
|
||||||
import SearchResults
|
import SearchResults
|
||||||
if handlerName in SearchResults.toolTipHandlers:
|
|
||||||
return SearchResults.toolTipHandlers[handlerName](nfo, setParent)
|
if handlerName in SearchResults.toolTipHandlers:
|
||||||
else:
|
return SearchResults.toolTipHandlers[handlerName](nfo, setParent)
|
||||||
return 'Could not load tooltip for this tool, it could be from a Mod that has been uninstalled. Try refreshing the list of tools.'
|
else:
|
||||||
|
return translate(
|
||||||
|
"SearchBar",
|
||||||
|
"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():
|
def getItemGroups():
|
||||||
global itemGroups, serializedItemGroups, globalGroups
|
global itemGroups, serializedItemGroups, globalGroups
|
||||||
|
|
||||||
# Import the tooltip+action handlers and search result providers that are bundled with this Mod.
|
# 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
|
# Other providers should import SearchResults and register their handlers and providers
|
||||||
import BuiltInSearchResults
|
import BuiltInSearchResults
|
||||||
|
|
||||||
# Load the list of tools, preferably from the cache, if it has not already been loaded:
|
# Load the list of tools, preferably from the cache, if it has not already been loaded:
|
||||||
if itemGroups is None:
|
if itemGroups is None:
|
||||||
if serializedItemGroups is None:
|
if serializedItemGroups is None:
|
||||||
import RefreshTools
|
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
|
itemGroups = RefreshTools.refreshToolbars(doLoadAllWorkbenches=False)
|
||||||
import SearchResults
|
else:
|
||||||
igs = itemGroups
|
import Serialize_SearchBar
|
||||||
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
|
itemGroups = Serialize_SearchBar.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 PySide import QtGui
|
||||||
|
|
||||||
|
|
||||||
# Inspired by https://stackoverflow.com/a/5443220/324969
|
# Inspired by https://stackoverflow.com/a/5443220/324969
|
||||||
# Inspired by https://forum.qt.io/topic/69807/qtreeview-indent-entire-row
|
# Inspired by https://forum.qt.io/topic/69807/qtreeview-indent-entire-row
|
||||||
class IndentedItemDelegate(QtGui.QStyledItemDelegate):
|
class IndentedItemDelegate(QtGui.QStyledItemDelegate):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(IndentedItemDelegate, self).__init__()
|
super(IndentedItemDelegate, self).__init__()
|
||||||
def paint(self, painter, option, index):
|
|
||||||
depth = int(option.widget.model().itemData(index.siblingAtColumn(1))[0])
|
def paint(self, painter, option, index):
|
||||||
indent = 16 * depth
|
depth = int(option.widget.model().itemData(index.siblingAtColumn(1))[0])
|
||||||
option.rect.adjust(indent, 0, 0, 0)
|
indent = 16 * depth
|
||||||
super(IndentedItemDelegate, self).paint(painter, option, index)
|
option.rect.adjust(indent, 0, 0, 0)
|
||||||
|
super(IndentedItemDelegate, self).paint(painter, option, index)
|
||||||
|
|
56
InitGui.py
|
@ -1,37 +1,39 @@
|
||||||
|
import FreeCAD as App
|
||||||
|
import FreeCADGui as Gui
|
||||||
|
|
||||||
# Avoid garbage collection by storing the action in a global variable
|
# Avoid garbage collection by storing the action in a global variable
|
||||||
wax = None
|
wax = None
|
||||||
sea = None
|
sea = None
|
||||||
tbr = None
|
tbr = None
|
||||||
|
|
||||||
|
# Define the translation
|
||||||
|
translate = App.Qt.translate
|
||||||
|
|
||||||
|
|
||||||
|
def QT_TRANSLATE_NOOP(context, text):
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
def addToolSearchBox():
|
def addToolSearchBox():
|
||||||
import FreeCADGui
|
global wax, sea, tbr
|
||||||
from PySide import QtGui
|
mw = Gui.getMainWindow()
|
||||||
import SearchBoxLight
|
import SearchBox
|
||||||
global wax, sea, tbr
|
from PySide.QtWidgets import QToolBar
|
||||||
mw = FreeCADGui.getMainWindow()
|
from PySide.QtGui import QShortcut, QKeySequence
|
||||||
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:
|
if mw:
|
||||||
wax = QtGui.QWidgetAction(None)
|
if sea is None:
|
||||||
wax.setWhatsThis('Use this search bar to find tools, document objects, preferences and more')
|
wax = SearchBox.SearchBoxFunction(mw)
|
||||||
|
if tbr is None:
|
||||||
|
tbr = 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()
|
||||||
|
return
|
||||||
|
|
||||||
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()
|
addToolSearchBox()
|
||||||
import FreeCADGui
|
Gui.getMainWindow().workbenchActivated.connect(addToolSearchBox)
|
||||||
FreeCADGui.getMainWindow().workbenchActivated.connect(addToolSearchBox)
|
|
||||||
|
|
80
Parameters_SearchBar.py
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import FreeCAD as App
|
||||||
|
import FreeCADGui as Gui
|
||||||
|
from PySide.QtGui import QColor
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Define the translation
|
||||||
|
translate = App.Qt.translate
|
||||||
|
|
||||||
|
preferences = App.ParamGet("User parameter:BaseApp/Preferences/Mod/FreeCAD-Ribbon")
|
||||||
|
|
||||||
|
|
||||||
|
class Settings:
|
||||||
|
|
||||||
|
# region -- Functions to read the settings from the FreeCAD Parameters
|
||||||
|
# and make sure that a None type result is ""
|
||||||
|
def GetStringSetting(settingName: str) -> str:
|
||||||
|
result = preferences.GetString(settingName)
|
||||||
|
|
||||||
|
if result.lower() == "none":
|
||||||
|
result = ""
|
||||||
|
return result
|
||||||
|
|
||||||
|
def GetIntSetting(settingName: str) -> int:
|
||||||
|
result = preferences.GetInt(settingName)
|
||||||
|
if result == "":
|
||||||
|
result = None
|
||||||
|
return result
|
||||||
|
|
||||||
|
def GetFloatSetting(settingName: str) -> int:
|
||||||
|
result = preferences.GetFloat(settingName)
|
||||||
|
if result == "":
|
||||||
|
result = None
|
||||||
|
return result
|
||||||
|
|
||||||
|
def GetBoolSetting(settingName: str) -> bool:
|
||||||
|
result = preferences.GetBool(settingName)
|
||||||
|
if str(result).lower() == "none":
|
||||||
|
result = False
|
||||||
|
return result
|
||||||
|
|
||||||
|
def GetColorSetting(settingName: str) -> object:
|
||||||
|
# Create a tuple from the int value of the color
|
||||||
|
result = QColor.fromRgba(preferences.GetUnsigned(settingName)).toTuple()
|
||||||
|
|
||||||
|
# correct the order of the tuple and divide them by 255
|
||||||
|
result = (result[3] / 255, result[0] / 255, result[1] / 255, result[2] / 255)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
# region - Functions to write settings to the FreeCAD Parameters
|
||||||
|
#
|
||||||
|
#
|
||||||
|
def SetStringSetting(settingName: str, value: str):
|
||||||
|
if value.lower() == "none":
|
||||||
|
value = ""
|
||||||
|
preferences.SetString(settingName, value)
|
||||||
|
return
|
||||||
|
|
||||||
|
def SetBoolSetting(settingName: str, value):
|
||||||
|
if str(value).lower() == "true":
|
||||||
|
Bool = True
|
||||||
|
if str(value).lower() == "none" or str(value).lower() != "true":
|
||||||
|
Bool = False
|
||||||
|
preferences.SetBool(settingName, Bool)
|
||||||
|
return
|
||||||
|
|
||||||
|
def SetIntSetting(settingName: str, value: int):
|
||||||
|
if str(value).lower() != "":
|
||||||
|
preferences.SetInt(settingName, value)
|
||||||
|
|
||||||
|
|
||||||
|
# region - Define the resources ----------------------------------------------------------------------------------------
|
||||||
|
ICON_LOCATION = os.path.join(os.path.dirname(__file__), "Resources", "Icons")
|
||||||
|
# endregion ------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# The pixmap for the general tool icon
|
||||||
|
genericToolIcon_Pixmap = os.path.join(ICON_LOCATION, "Tango-Tools-spanner-hammer.svg")
|
14
README.md
|
@ -14,22 +14,20 @@ It can be extended by other mods, by adding a new result provider.
|
||||||
|
|
||||||
The search bar appears next to the [`What's this?`](https://wiki.freecad.org/Std_WhatsThis) tool <a href="https://wiki.freecad.org/Std_WhatsThis"><img src="https://user-images.githubusercontent.com/4140247/156215976-5dfadb0c-cac4-44b2-8ad4-b67462a5f7fa.png" alt="drawing" width="20px" height="20px"/></a> in FreeCAD's default File toolbar.
|
The search bar appears next to the [`What's this?`](https://wiki.freecad.org/Std_WhatsThis) tool <a href="https://wiki.freecad.org/Std_WhatsThis"><img src="https://user-images.githubusercontent.com/4140247/156215976-5dfadb0c-cac4-44b2-8ad4-b67462a5f7fa.png" alt="drawing" width="20px" height="20px"/></a> in FreeCAD's default File toolbar.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
When using the search bar for the first time, it will contain only the tools of the workbenches which have already been loaded in FreeCAD.
|
When using the search bar for the first time, it will contain only the tools of the workbenches which have already been loaded in FreeCAD.
|
||||||
To include results from other workbenches, select the first search result "Refresh list of tools" which will load all FreeCAD workbenches
|
To include results from other workbenches, select the first search result "Refresh cached results" which will load all FreeCAD workbenches
|
||||||
and memorize their tools. After restarting FreeCAD, the search result will include the memorized tools, even if the workbenches have not
|
and memorize their tools. After restarting FreeCAD, the search result will include the memorized tools, even if the workbenches have not
|
||||||
been loaded yet. When selecting a tool from the search results, SearchBar will attempt to automatically load the workbenches which could
|
been loaded yet. When selecting a tool from the search results, SearchBar will attempt to automatically load the workbenches which could
|
||||||
have provided that tool.
|
have provided that tool.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
To navigate the search results, use the up and down arrows. Typing characters will filter the results on the fly. The extended information
|
To navigate the search results, use the up and down arrows. Typing characters will filter the results on the fly. The extended information
|
||||||
panel next to the search results provides further documentation about the results, e.g. Python snippets which can be copy-pasted (note:
|
panel next to the search results provides further documentation about the results, e.g. Python snippets which can be copy-pasted.
|
||||||
currently a bug crashes FreeCAD if using the context menu to perform the copy, please do not use the context menu until
|
|
||||||
https://github.com/SuzanneSoy/SearchBar/issues/12 is fixed.
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
|
@ -64,7 +62,7 @@ Clone the GIT repository or extract the `.zip` downloaded from GitHub to the fol
|
||||||
|
|
||||||
### Feedback
|
### Feedback
|
||||||
|
|
||||||
To report bugs or feature enhancements, please open a ticket in the [issue queue](https://github.com/SuzanneSoy/SearchBar/issues). Best place to discuss feedback or issues in on the [dedicated FreeCAD forum discussion]() for SearchBar.
|
To report bugs or feature enhancements, please open a ticket in the [issue queue](https://github.com/APEbbers/SearchBar/issues). Best place to discuss feedback or issues in on the [dedicated FreeCAD forum discussion]() for SearchBar.
|
||||||
|
|
||||||
### License [](https://creativecommons.org/publicdomain/zero/1.0/)
|
### License [](https://creativecommons.org/publicdomain/zero/1.0/)
|
||||||
See [LICENSE](LICENSE).
|
See [LICENSE](LICENSE).
|
||||||
|
|
172
RefreshTools.py
|
@ -1,81 +1,123 @@
|
||||||
import os
|
import os
|
||||||
import FreeCAD as App
|
import FreeCAD as App
|
||||||
|
import FreeCADGui as Gui
|
||||||
|
import StyleMapping_SearchBar
|
||||||
|
|
||||||
|
# Define the translation
|
||||||
|
translate = App.Qt.translate
|
||||||
|
|
||||||
|
|
||||||
def loadAllWorkbenches():
|
def loadAllWorkbenches():
|
||||||
from PySide import QtGui
|
import FreeCADGui as Gui
|
||||||
import FreeCADGui
|
from PySide.QtGui import QLabel
|
||||||
activeWorkbench = FreeCADGui.activeWorkbench().name()
|
from PySide.QtCore import Qt, SIGNAL, Signal, QObject, QThread, QSize
|
||||||
lbl = QtGui.QLabel('Loading workbench … (…/…)')
|
from PySide.QtGui import QIcon, QPixmap, QAction, QGuiApplication
|
||||||
lbl.show()
|
|
||||||
lst = FreeCADGui.listWorkbenches()
|
activeWorkbench = Gui.activeWorkbench().name()
|
||||||
for i, wb in enumerate(lst):
|
lbl = QLabel(translate("SearchBar", "Loading workbench … (…/…)"))
|
||||||
msg = 'Loading workbench ' + wb + ' (' + str(i) + '/' + str(len(lst)) + ')'
|
lbl.setWindowFlags(Qt.WindowType.WindowStaysOnTopHint)
|
||||||
print(msg)
|
|
||||||
lbl.setText(msg)
|
# Get the stylesheet from the main window and use it for this form
|
||||||
geo = lbl.geometry()
|
lbl.setStyleSheet("background-color: " + StyleMapping_SearchBar.ReturnStyleItem("Background_Color") + ";")
|
||||||
geo.setSize(lbl.sizeHint())
|
|
||||||
lbl.setGeometry(geo)
|
# # Get the main window from FreeCAD
|
||||||
lbl.repaint()
|
# mw = Gui.getMainWindow()
|
||||||
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…
|
# # Center the widget
|
||||||
try:
|
# cp = QGuiApplication.screenAt(mw.pos()).geometry().center()
|
||||||
FreeCADGui.activateWorkbench(wb)
|
# lbl.move(cp)
|
||||||
except:
|
|
||||||
pass
|
lbl.show()
|
||||||
lbl.hide()
|
lst = Gui.listWorkbenches()
|
||||||
FreeCADGui.activateWorkbench(activeWorkbench)
|
for i, wb in enumerate(lst):
|
||||||
|
msg = translate("SearchBar", "Loading workbench ") + wb + " (" + str(i + 1) + "/" + str(len(lst)) + ")"
|
||||||
|
print(msg)
|
||||||
|
lbl.setText(msg)
|
||||||
|
geo = lbl.geometry()
|
||||||
|
geo.setSize(lbl.sizeHint())
|
||||||
|
lbl.setGeometry(geo)
|
||||||
|
lbl.repaint()
|
||||||
|
Gui.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:
|
||||||
|
Gui.activateWorkbench(wb)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
lbl.hide()
|
||||||
|
Gui.activateWorkbench(activeWorkbench)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def cachePath():
|
def cachePath():
|
||||||
return os.path.join(App.getUserAppDataDir(), 'Cache_SearchBarMod')
|
return os.path.join(App.getUserAppDataDir(), "Cache_SearchBarMod")
|
||||||
|
|
||||||
|
|
||||||
def gatherTools():
|
def gatherTools():
|
||||||
itemGroups = []
|
itemGroups = []
|
||||||
import SearchResults
|
import SearchResults
|
||||||
for providerName, provider in SearchResults.resultProvidersCached.items():
|
|
||||||
itemGroups = itemGroups + provider()
|
for providerName, provider in SearchResults.resultProvidersCached.items():
|
||||||
return itemGroups
|
itemGroups = itemGroups + provider()
|
||||||
|
return itemGroups
|
||||||
|
|
||||||
|
|
||||||
def writeCacheTools():
|
def writeCacheTools():
|
||||||
import Serialize
|
import Serialize_SearchBar
|
||||||
serializedItemGroups = Serialize.serialize(gatherTools())
|
|
||||||
# Todo: use wb and a specific encoding.
|
serializedItemGroups = Serialize_SearchBar.serialize(gatherTools())
|
||||||
with open(cachePath(), 'w') as cache:
|
# Todo: use wb and a specific encoding.
|
||||||
cache.write(serializedItemGroups)
|
with open(cachePath(), "w") as cache:
|
||||||
# I prefer to systematically deserialize, instead of taking the original version,
|
cache.write(serializedItemGroups)
|
||||||
# this avoids possible inconsistencies between the original and the cache and
|
# I prefer to systematically deserialize, instead of taking the original version,
|
||||||
# makes sure cache-related bugs are noticed quickly.
|
# this avoids possible inconsistencies between the original and the cache and
|
||||||
import Serialize
|
# makes sure cache-related bugs are noticed quickly.
|
||||||
itemGroups = Serialize.deserialize(serializedItemGroups)
|
import Serialize_SearchBar
|
||||||
print('SearchBox: Cache has been written.')
|
|
||||||
return itemGroups
|
itemGroups = Serialize_SearchBar.deserialize(serializedItemGroups)
|
||||||
|
print("SearchBox: Cache has been written.")
|
||||||
|
return itemGroups
|
||||||
|
|
||||||
|
|
||||||
def readCacheTools():
|
def readCacheTools():
|
||||||
# Todo: use rb and a specific encoding.
|
# Todo: use rb and a specific encoding.
|
||||||
with open(cachePath(), 'r') as cache:
|
with open(cachePath(), "r") as cache:
|
||||||
serializedItemGroups = cache.read()
|
serializedItemGroups = cache.read()
|
||||||
import Serialize
|
import Serialize_SearchBar
|
||||||
itemGroups = Serialize.deserialize(serializedItemGroups)
|
|
||||||
print('SearchBox: Tools were loaded from the cache.')
|
itemGroups = Serialize_SearchBar.deserialize(serializedItemGroups)
|
||||||
return itemGroups
|
print("SearchBox: Tools were loaded from the cache.")
|
||||||
|
return itemGroups
|
||||||
|
|
||||||
|
|
||||||
def refreshToolbars(doLoadAllWorkbenches = True):
|
def refreshToolbars(doLoadAllWorkbenches=True):
|
||||||
if doLoadAllWorkbenches:
|
if doLoadAllWorkbenches:
|
||||||
loadAllWorkbenches()
|
loadAllWorkbenches()
|
||||||
return writeCacheTools()
|
return writeCacheTools()
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
return readCacheTools()
|
return readCacheTools()
|
||||||
except:
|
except:
|
||||||
return writeCacheTools()
|
return writeCacheTools()
|
||||||
|
|
||||||
|
|
||||||
def refreshToolsAction():
|
def refreshToolsAction():
|
||||||
from PySide import QtGui
|
from PySide.QtWidgets import QApplication, QMessageBox
|
||||||
print('Refresh list of tools')
|
from PySide.QtCore import Qt
|
||||||
fw = QtGui.QApplication.focusWidget()
|
|
||||||
if fw is not None:
|
print("Refresh cached results")
|
||||||
fw.clearFocus()
|
msgBox = QMessageBox()
|
||||||
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)
|
msgBox.setWindowFlags(Qt.WindowType.WindowStaysOnTopHint)
|
||||||
if reply == QtGui.QMessageBox.Yes:
|
# Get the main window from FreeCAD
|
||||||
refreshToolbars()
|
mw = Gui.getMainWindow()
|
||||||
else:
|
reply = msgBox.question(
|
||||||
print('cancelled')
|
mw,
|
||||||
|
translate("SearchBar", "Load all workbenches?"),
|
||||||
|
translate(
|
||||||
|
"SearchBar",
|
||||||
|
"""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.""",
|
||||||
|
),
|
||||||
|
QMessageBox.Yes,
|
||||||
|
QMessageBox.No,
|
||||||
|
)
|
||||||
|
if reply == QMessageBox.Yes:
|
||||||
|
refreshToolbars()
|
||||||
|
else:
|
||||||
|
print("cancelled")
|
||||||
|
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 683 KiB After Width: | Height: | Size: 683 KiB |
Before Width: | Height: | Size: 404 KiB After Width: | Height: | Size: 404 KiB |
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
@ -5,112 +5,157 @@ import FreeCADGui
|
||||||
import SafeViewer
|
import SafeViewer
|
||||||
import SearchBox
|
import SearchBox
|
||||||
|
|
||||||
|
# Define the translation
|
||||||
|
translate = App.Qt.translate
|
||||||
|
|
||||||
|
|
||||||
def documentAction(nfo):
|
def documentAction(nfo):
|
||||||
act = nfo['action']
|
act = nfo["action"]
|
||||||
# Todo: this should also select the document in the tree view
|
# Todo: this should also select the document in the tree view
|
||||||
print('switch to document ' + act['document'])
|
print("switch to document " + act["document"])
|
||||||
App.setActiveDocument(act['document'])
|
App.setActiveDocument(act["document"])
|
||||||
|
|
||||||
|
|
||||||
def documentObjectAction(nfo):
|
def documentObjectAction(nfo):
|
||||||
act = nfo['action']
|
act = nfo["action"]
|
||||||
print('select object ' + act['document'] + '.' + act['object'])
|
print("select object " + act["document"] + "." + act["object"])
|
||||||
FreeCADGui.Selection.addSelection(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.
|
# 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.
|
# 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.
|
# 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'):
|
if not hasattr(App, "_SearchBar3DViewer"):
|
||||||
# Toggle between
|
# Toggle between
|
||||||
App._SearchBar3DViewer = None
|
App._SearchBar3DViewer = None
|
||||||
App._SearchBar3DViewerB = None
|
App._SearchBar3DViewerB = None
|
||||||
|
|
||||||
|
|
||||||
class DocumentObjectToolTipWidget(QtGui.QWidget):
|
class DocumentObjectToolTipWidget(QtGui.QWidget):
|
||||||
def __init__(self, nfo, setParent):
|
def __init__(self, nfo, setParent):
|
||||||
import pivy
|
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)
|
|
||||||
|
|
||||||
if App._SearchBar3DViewer is None:
|
super(DocumentObjectToolTipWidget, self).__init__()
|
||||||
oldFocus = QtGui.QApplication.focusWidget()
|
html = (
|
||||||
SearchBox.globalIgnoreFocusOut
|
"<p>"
|
||||||
SearchBox.globalIgnoreFocusOut = True
|
+ nfo["toolTip"]["label"]
|
||||||
App._SearchBar3DViewer = SafeViewer.SafeViewer()
|
+ "</p><p><code>App.getDocument("
|
||||||
App._SearchBar3DViewerB = SafeViewer.SafeViewer()
|
+ repr(str(nfo["toolTip"]["docName"]))
|
||||||
oldFocus.setFocus()
|
+ ").getObject("
|
||||||
SearchBox.globalIgnoreFocusOut = False
|
+ repr(str(nfo["toolTip"]["name"]))
|
||||||
# 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.
|
+ ")</code></p>"
|
||||||
#safeViewerInstance.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed))
|
)
|
||||||
self.preview = App._SearchBar3DViewer
|
description = QtGui.QTextEdit()
|
||||||
App._SearchBar3DViewer, App._SearchBar3DViewerB = App._SearchBar3DViewerB, App._SearchBar3DViewer
|
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
|
obj = App.getDocument(str(nfo["toolTip"]["docName"])).getObject(
|
||||||
# finalizing the object, we remove the parent ourselves.
|
str(nfo["toolTip"]["name"])
|
||||||
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
|
# This is really a bad way to do this… to prevent the setExtraInfo function from
|
||||||
#self.preview.viewer.stopAnimating()
|
# finalizing the object, we remove the parent ourselves.
|
||||||
self.preview.showSceneGraph(obj.ViewObject.RootNode)
|
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)
|
||||||
|
|
||||||
setParent(self)
|
# Tried hiding/detaching the preview to prevent it from disappearing when changing its contents
|
||||||
# Let the GUI recompute the side of the description based on its horizontal size.
|
# self.preview.viewer.stopAnimating()
|
||||||
FreeCADGui.updateGui()
|
self.preview.showSceneGraph(obj.ViewObject.RootNode)
|
||||||
siz = description.document().size().toSize()
|
|
||||||
description.setFixedHeight(siz.height() + 5)
|
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):
|
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):
|
def documentObjectToolTip(nfo, setParent):
|
||||||
return DocumentObjectToolTipWidget(nfo, setParent)
|
return DocumentObjectToolTipWidget(nfo, setParent)
|
||||||
|
|
||||||
|
|
||||||
def documentResultsProvider():
|
def documentResultsProvider():
|
||||||
itemGroups = []
|
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 }
|
def document(doc):
|
||||||
itemGroups.append({
|
group = []
|
||||||
'icon': QtGui.QIcon(':/icons/Document.svg'),
|
for o in doc.Objects:
|
||||||
'text': doc.Label + ' (' + doc.Name + ')',
|
# all_actions.append(lambda: )
|
||||||
# TODO: preview of the document
|
action = {
|
||||||
'toolTip': { 'label': doc.Label, 'name': doc.Name},
|
"handler": "documentObject",
|
||||||
'action':action,
|
"document": o.Document.Name,
|
||||||
'subitems': group })
|
"object": o.Name,
|
||||||
if App.ActiveDocument:
|
}
|
||||||
document(App.ActiveDocument)
|
item = {
|
||||||
for docname, doc in App.listDocuments().items():
|
"icon": (
|
||||||
if not App.activeDocument or docname != App.ActiveDocument.Name:
|
o.ViewObject.Icon if o.ViewObject and o.ViewObject.Icon else None
|
||||||
document(doc)
|
),
|
||||||
return itemGroups
|
"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
|
||||||
|
|
|
@ -2,89 +2,127 @@ import os
|
||||||
import FreeCAD as App
|
import FreeCAD as App
|
||||||
import FreeCADGui
|
import FreeCADGui
|
||||||
from PySide import QtGui
|
from PySide import QtGui
|
||||||
import Serialize
|
import Serialize_SearchBar
|
||||||
|
import Parameters_SearchBar as Parameters
|
||||||
|
|
||||||
|
genericToolIcon = QtGui.QIcon(QtGui.QIcon(Parameters.genericToolIcon_Pixmap))
|
||||||
|
|
||||||
genericToolIcon = QtGui.QIcon(QtGui.QIcon(os.path.dirname(__file__) + '/Tango-Tools-spanner-hammer.svg'))
|
|
||||||
|
|
||||||
def getParam(grpPath, type_, name):
|
def getParam(grpPath, type_, name):
|
||||||
return {
|
return {
|
||||||
# TODO: use letter icon based on the type, as the preferences editor does
|
# TODO: use letter icon based on the type, as the preferences editor does
|
||||||
'icon': genericToolIcon,
|
"icon": genericToolIcon,
|
||||||
'text': name,
|
"text": name,
|
||||||
'toolTip': '',
|
"toolTip": "",
|
||||||
'action': {'handler': 'param', 'path': grpPath, 'type': type_, 'name': name},
|
"action": {"handler": "param", "path": grpPath, "type": type_, "name": name},
|
||||||
'subitems': []
|
"subitems": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def getParamGroup(grpPath):
|
def getParamGroup(grpPath):
|
||||||
try:
|
try:
|
||||||
grp = App.ParamGet(grpPath)
|
grp = App.ParamGet(grpPath)
|
||||||
except:
|
except:
|
||||||
return []
|
return []
|
||||||
contents = grp.GetContents()
|
contents = grp.GetContents()
|
||||||
if contents is not None:
|
if contents is not None:
|
||||||
return [getParam(grpPath, type_, name) for (type_, name, value) in contents]
|
return [getParam(grpPath, type_, name) for (type_, name, value) in contents]
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def getParamGroups(nameInConfig, nameInPath):
|
def getParamGroups(nameInConfig, nameInPath):
|
||||||
userParameterPath = App.ConfigGet(nameInConfig)
|
userParameterPath = App.ConfigGet(nameInConfig)
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
xml = etree.parse(userParameterPath).getroot()
|
xml = etree.parse(userParameterPath).getroot()
|
||||||
xml.find('FCParamGroup[@Name="Root"]')
|
xml.find('FCParamGroup[@Name="Root"]')
|
||||||
root = xml.find('FCParamGroup[@Name="Root"]')
|
root = xml.find('FCParamGroup[@Name="Root"]')
|
||||||
|
|
||||||
def recur(atRoot, path, name, tree):
|
def recur(atRoot, path, name, tree):
|
||||||
params = [] if atRoot else getParamGroup(path)
|
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']
|
subgroups = [
|
||||||
return {
|
recur(
|
||||||
'icon': QtGui.QIcon(':/icons/Group.svg'),
|
False,
|
||||||
'text': name,
|
path + (":" if atRoot else "/") + child.attrib["Name"],
|
||||||
'toolTip': '',
|
child.attrib["Name"],
|
||||||
'action': { 'handler': 'paramGroup', 'path': path, 'name': name },
|
child,
|
||||||
'subitems': params + subgroups
|
)
|
||||||
}
|
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)
|
return recur(True, nameInPath, nameInPath, root)
|
||||||
|
|
||||||
|
|
||||||
def getAllParams():
|
def getAllParams():
|
||||||
try:
|
try:
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
return [getParamGroups('UserParameter', 'User parameter')]
|
|
||||||
except:
|
return [getParamGroups("UserParameter", "User parameter")]
|
||||||
print('Could not load the list of all parameters. Please install the LXML python library with:\npython -m pip install --upgrade lxml')
|
except:
|
||||||
return []
|
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):
|
def paramGroupAction(nfo):
|
||||||
FreeCADGui.runCommand('Std_DlgParameter',0)
|
FreeCADGui.runCommand("Std_DlgParameter", 0)
|
||||||
print('Open Parameter Editor (parameter group)')
|
print("Open Parameter Editor (parameter group)")
|
||||||
# TODO: find a way to select the desired group in the parameter dialog once it opens
|
# TODO: find a way to select the desired group in the parameter dialog once it opens
|
||||||
|
|
||||||
|
|
||||||
def paramAction(nfo):
|
def paramAction(nfo):
|
||||||
FreeCADGui.runCommand('Std_DlgParameter',0)
|
FreeCADGui.runCommand("Std_DlgParameter", 0)
|
||||||
print('Open Parameter Editor (single parameter)')
|
print("Open Parameter Editor (single parameter)")
|
||||||
# TODO: find a way to select the desired parameter in the parameter dialog once it opens
|
# TODO: find a way to select the desired parameter in the parameter dialog once it opens
|
||||||
|
|
||||||
|
|
||||||
getters = {
|
getters = {
|
||||||
'Boolean' : 'GetBool',
|
"Boolean": "GetBool",
|
||||||
'Float' : 'GetFloat',
|
"Float": "GetFloat",
|
||||||
'Integer' : 'GetInt',
|
"Integer": "GetInt",
|
||||||
'String' : 'GetString',
|
"String": "GetString",
|
||||||
'Unsigned Long': 'GetUnsigned',
|
"Unsigned Long": "GetUnsigned",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def paramGroupToolTip(nfo, setParent):
|
def paramGroupToolTip(nfo, setParent):
|
||||||
path = nfo['action']['path']
|
path = nfo["action"]["path"]
|
||||||
name = nfo['action']['name']
|
name = nfo["action"]["name"]
|
||||||
return '<h1>' + name + '</h1><p><code>App.ParamGet(' + repr(path) + ')</code></p'
|
return "<h1>" + name + "</h1><p><code>App.ParamGet(" + repr(path) + ")</code></p"
|
||||||
|
|
||||||
|
|
||||||
def paramToolTip(nfo, setParent):
|
def paramToolTip(nfo, setParent):
|
||||||
path = nfo['action']['path']
|
path = nfo["action"]["path"]
|
||||||
type_ = nfo['action']['type']
|
type_ = nfo["action"]["type"]
|
||||||
name = nfo['action']['name']
|
name = nfo["action"]["name"]
|
||||||
try:
|
try:
|
||||||
value = getattr(App.ParamGet(path), getters[type_])(name)
|
value = getattr(App.ParamGet(path), getters[type_])(name)
|
||||||
except:
|
except:
|
||||||
value = 'An error occurred while attempting to access this value.'
|
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>'
|
return (
|
||||||
|
"<p><code>App.ParamGet("
|
||||||
|
+ repr(path)
|
||||||
|
+ ")."
|
||||||
|
+ getters[type_]
|
||||||
|
+ "("
|
||||||
|
+ repr(name)
|
||||||
|
+ ")</code></p><p>Type: "
|
||||||
|
+ type_
|
||||||
|
+ "</p><p>Value: "
|
||||||
|
+ repr(value)
|
||||||
|
+ "</p>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def paramResultsProvider():
|
def paramResultsProvider():
|
||||||
return getAllParams()
|
return getAllParams()
|
||||||
|
|
|
@ -1,22 +1,42 @@
|
||||||
|
import FreeCAD as App
|
||||||
|
import FreeCADGui as Gui
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from PySide import QtGui
|
from PySide import QtGui
|
||||||
import Serialize
|
import Serialize_SearchBar
|
||||||
|
import Parameters_SearchBar as Parameters
|
||||||
|
|
||||||
|
genericToolIcon = QtGui.QIcon(QtGui.QIcon(Parameters.genericToolIcon_Pixmap))
|
||||||
|
|
||||||
|
# Define the translation
|
||||||
|
translate = App.Qt.translate
|
||||||
|
|
||||||
|
|
||||||
def refreshToolsAction(nfo):
|
def refreshToolsAction(nfo):
|
||||||
import RefreshTools
|
import RefreshTools
|
||||||
RefreshTools.refreshToolsAction()
|
|
||||||
|
RefreshTools.refreshToolsAction()
|
||||||
|
|
||||||
|
|
||||||
def refreshToolsToolTip(nfo, setParent):
|
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_SearchBar.iconToHTML(genericToolIcon)
|
||||||
|
+ "<p>"
|
||||||
|
+ translate(
|
||||||
|
"SearchBar",
|
||||||
|
"Load all workbenches to refresh the cached results. 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'))
|
|
||||||
def refreshToolsResultsProvider():
|
def refreshToolsResultsProvider():
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'icon': genericToolIcon,
|
"icon": genericToolIcon,
|
||||||
'text': 'Refresh list of tools',
|
"text": translate("SearchBar", "Refresh cached results"),
|
||||||
'toolTip': '',
|
"toolTip": "",
|
||||||
'action': {'handler': 'refreshTools'},
|
"action": {"handler": "refreshTools"},
|
||||||
'subitems': []
|
"subitems": [],
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,105 +1,195 @@
|
||||||
|
import FreeCAD as App
|
||||||
from PySide import QtGui
|
from PySide import QtGui
|
||||||
import FreeCADGui
|
import FreeCADGui
|
||||||
import Serialize
|
import Serialize_SearchBar
|
||||||
|
|
||||||
|
# Define the translation
|
||||||
|
translate = App.Qt.translate
|
||||||
|
|
||||||
|
|
||||||
def toolbarAction(nfo):
|
def toolbarAction(nfo):
|
||||||
act = nfo['action']
|
act = nfo["action"]
|
||||||
print('show toolbar ' + act['toolbar'] + ' from workbenches ' + repr(act['workbenches']))
|
print(
|
||||||
|
"show toolbar "
|
||||||
|
+ act["toolbar"]
|
||||||
|
+ " from workbenches "
|
||||||
|
+ repr(act["workbenches"])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def subToolAction(nfo):
|
def subToolAction(nfo):
|
||||||
act = nfo['action']
|
act = nfo["action"]
|
||||||
toolPath = act['toolbar'] + '.' + act['tool']
|
toolPath = act["toolbar"] + "." + act["tool"]
|
||||||
if 'subTool' in act:
|
if "subTool" in act:
|
||||||
toolPath = toolPath + '.' + act['subTool']
|
toolPath = toolPath + "." + act["subTool"]
|
||||||
def runTool():
|
|
||||||
mw = FreeCADGui.getMainWindow()
|
def runTool():
|
||||||
for the_toolbar in mw.findChildren(QtGui.QToolBar, act['toolbar']):
|
mw = FreeCADGui.getMainWindow()
|
||||||
for tbt in the_toolbar.findChildren(QtGui.QToolButton):
|
for the_toolbar in mw.findChildren(QtGui.QToolBar, act["toolbar"]):
|
||||||
if tbt.text() == act['tool']:
|
for tbt in the_toolbar.findChildren(QtGui.QToolButton):
|
||||||
action = None
|
if tbt.text() == act["tool"]:
|
||||||
if 'subTool' in act:
|
action = None
|
||||||
men = tbt.menu()
|
if "subTool" in act:
|
||||||
if men:
|
men = tbt.menu()
|
||||||
for mac in men.actions():
|
if men:
|
||||||
if mac.text() == act['subTool']:
|
for mac in men.actions():
|
||||||
action = mac
|
if mac.text() == act["subTool"]:
|
||||||
break
|
action = mac
|
||||||
else:
|
break
|
||||||
action = tbt.defaultAction()
|
else:
|
||||||
if 'showMenu' in act and act['showMenu']:
|
action = tbt.defaultAction()
|
||||||
print('Popup submenu of tool ' + toolPath + ' available in workbenches ' + repr(act['workbenches']))
|
if "showMenu" in act and act["showMenu"]:
|
||||||
the_toolbar.show()
|
print(
|
||||||
tbt.showMenu()
|
"Popup submenu of tool "
|
||||||
return True
|
+ toolPath
|
||||||
elif action is not None:
|
+ " available in workbenches "
|
||||||
print('Run action of tool ' + toolPath + ' available in workbenches ' + repr(act['workbenches']))
|
+ repr(act["workbenches"])
|
||||||
action.trigger()
|
)
|
||||||
return True
|
the_toolbar.show()
|
||||||
return False
|
tbt.showMenu()
|
||||||
if runTool():
|
return True
|
||||||
return
|
elif action is not None:
|
||||||
else:
|
print(
|
||||||
for workbench in act['workbenches']:
|
"Run action of tool "
|
||||||
print('Activating workbench ' + workbench + ' to access tool ' + toolPath)
|
+ toolPath
|
||||||
FreeCADGui.activateWorkbench(workbench)
|
+ " available in workbenches "
|
||||||
if runTool():
|
+ repr(act["workbenches"])
|
||||||
|
)
|
||||||
|
action.trigger()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
if runTool():
|
||||||
return
|
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)
|
||||||
|
try:
|
||||||
|
FreeCADGui.activateWorkbench(workbench)
|
||||||
|
except Exception:
|
||||||
|
print("SearchBar: Workbench not present! Was it disabled?")
|
||||||
|
return
|
||||||
|
if runTool():
|
||||||
|
return
|
||||||
|
print(
|
||||||
|
"Tool "
|
||||||
|
+ toolPath
|
||||||
|
+ " not found, was it offered by an extension that is no longer present?"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def toolbarToolTip(nfo, setParent):
|
def toolbarToolTip(nfo, setParent):
|
||||||
workbenches = FreeCADGui.listWorkbenches()
|
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']]
|
in_workbenches = [
|
||||||
return '<p>Show the ' + nfo['text'] + ' toolbar</p><p>This toolbar appears in the following workbenches: <ul>' + ''.join(in_workbenches) + '</ul></p>'
|
"<li>"
|
||||||
|
+ (
|
||||||
|
Serialize_SearchBar.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):
|
def subToolToolTip(nfo, setParent):
|
||||||
return Serialize.iconToHTML(nfo['icon'], 32) + '<p>' + nfo['toolTip'] + '</p>'
|
return (
|
||||||
|
Serialize_SearchBar.iconToHTML(nfo["icon"], 32)
|
||||||
|
+ "<p>"
|
||||||
|
+ nfo["toolTip"]
|
||||||
|
+ "</p>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def getAllToolbars():
|
def getAllToolbars():
|
||||||
all_tbs = dict()
|
all_tbs = dict()
|
||||||
for wbname, workbench in FreeCADGui.listWorkbenches().items():
|
for wbname, workbench in FreeCADGui.listWorkbenches().items():
|
||||||
try:
|
try:
|
||||||
tbs = workbench.listToolbars()
|
tbs = workbench.listToolbars()
|
||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
# careful, tbs contains all the toolbars of the workbench, including shared toolbars
|
# careful, tbs contains all the toolbars of the workbench, including shared toolbars
|
||||||
for tb in tbs:
|
for tb in tbs:
|
||||||
if tb not in all_tbs:
|
if tb not in all_tbs:
|
||||||
all_tbs[tb] = set()
|
all_tbs[tb] = set()
|
||||||
all_tbs[tb].add(wbname)
|
all_tbs[tb].add(wbname)
|
||||||
return all_tbs
|
return all_tbs
|
||||||
|
|
||||||
|
|
||||||
def toolbarResultsProvider():
|
def toolbarResultsProvider():
|
||||||
itemGroups = []
|
itemGroups = []
|
||||||
all_tbs = getAllToolbars()
|
all_tbs = getAllToolbars()
|
||||||
mw = FreeCADGui.getMainWindow()
|
mw = FreeCADGui.getMainWindow()
|
||||||
for toolbarName, toolbarIsInWorkbenches in all_tbs.items():
|
for toolbarName, toolbarIsInWorkbenches in all_tbs.items():
|
||||||
toolbarIsInWorkbenches = sorted(list(toolbarIsInWorkbenches))
|
toolbarIsInWorkbenches = sorted(list(toolbarIsInWorkbenches))
|
||||||
for the_toolbar in mw.findChildren(QtGui.QToolBar, toolbarName):
|
for the_toolbar in mw.findChildren(QtGui.QToolBar, toolbarName):
|
||||||
group = []
|
group = []
|
||||||
for tbt in the_toolbar.findChildren(QtGui.QToolButton):
|
for tbt in the_toolbar.findChildren(QtGui.QToolButton):
|
||||||
text = tbt.text()
|
text = tbt.text()
|
||||||
act = tbt.defaultAction()
|
act = tbt.defaultAction()
|
||||||
if text != '':
|
if text != "":
|
||||||
# TODO: there also is the tooltip
|
# TODO: there also is the tooltip
|
||||||
icon = tbt.icon()
|
icon = tbt.icon()
|
||||||
men = tbt.menu()
|
men = tbt.menu()
|
||||||
subgroup = []
|
subgroup = []
|
||||||
if men:
|
if men:
|
||||||
subgroup = []
|
subgroup = []
|
||||||
for mac in men.actions():
|
for mac in men.actions():
|
||||||
if mac.text():
|
if mac.text():
|
||||||
action = { 'handler': 'subTool', 'workbenches': toolbarIsInWorkbenches, 'toolbar': toolbarName, 'tool': text, 'subTool': mac.text() }
|
action = {
|
||||||
subgroup.append({'icon':mac.icon(), 'text':mac.text(), 'toolTip': mac.toolTip(), 'action':action, 'subitems':[]})
|
"handler": "subTool",
|
||||||
# The default action of a menu changes dynamically, instead of triggering the last action, just show the menu.
|
"workbenches": toolbarIsInWorkbenches,
|
||||||
action = { 'handler': 'tool', 'workbenches': toolbarIsInWorkbenches, 'toolbar': toolbarName, 'tool': text, 'showMenu': bool(men) }
|
"toolbar": toolbarName,
|
||||||
group.append({'icon':icon, 'text':text, 'toolTip': tbt.toolTip(), 'action': action, 'subitems': subgroup})
|
"tool": text,
|
||||||
# TODO: move the 'workbenches' field to the itemgroup
|
"subTool": mac.text(),
|
||||||
action = { 'handler': 'toolbar', 'workbenches': toolbarIsInWorkbenches, 'toolbar': toolbarName }
|
}
|
||||||
itemGroups.append({
|
subgroup.append(
|
||||||
'icon': QtGui.QIcon(':/icons/Group.svg'),
|
{
|
||||||
'text': toolbarName,
|
"icon": mac.icon(),
|
||||||
'toolTip': '',
|
"text": mac.text(),
|
||||||
'action': action,
|
"toolTip": mac.toolTip(),
|
||||||
'subitems': group
|
"action": action,
|
||||||
})
|
"subitems": [],
|
||||||
return itemGroups
|
}
|
||||||
|
)
|
||||||
|
# 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
|
||||||
|
|
205
SafeViewer.py
|
@ -1,106 +1,137 @@
|
||||||
from PySide import QtGui
|
from PySide import QtGui
|
||||||
import FreeCAD
|
import FreeCAD as App
|
||||||
|
|
||||||
|
# Define the translation
|
||||||
|
translate = App.Qt.translate
|
||||||
|
|
||||||
|
|
||||||
class SafeViewer(QtGui.QWidget):
|
class SafeViewer(QtGui.QWidget):
|
||||||
"""FreeCAD uses a modified version of QuarterWidget, so the import pivy.quarter one will cause segfaults.
|
"""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.
|
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."""
|
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):
|
enabled = App.ParamGet("User parameter:BaseApp/Preferences/Mod/SearchBar").GetBool(
|
||||||
if not SafeViewer.enabled:
|
"PreviewEnabled", False
|
||||||
for instance in SafeViewer.instances:
|
)
|
||||||
instance.enable()
|
instances = []
|
||||||
|
|
||||||
def enable_for_future_sessions(self):
|
def __init__(self, parent=None):
|
||||||
if not SafeViewer.enabled:
|
super(SafeViewer, self).__init__()
|
||||||
# Store in prefs
|
SafeViewer.instances.append(self)
|
||||||
FreeCAD.ParamGet('User parameter:BaseApp/Preferences/Mod/SearchBar').SetBool('PreviewEnabled', True)
|
self.init_parent = parent
|
||||||
# Then enable as usual
|
self.instance_enabled = False # Has this specific instance been enabled?
|
||||||
self.enable_for_this_session()
|
if SafeViewer.enabled:
|
||||||
|
self.displaying_warning = False
|
||||||
|
self.enable()
|
||||||
|
else:
|
||||||
|
import FreeCADGui
|
||||||
|
from PySide import QtCore
|
||||||
|
|
||||||
def enable(self):
|
self.displaying_warning = True
|
||||||
if not self.instance_enabled:
|
self.lbl_warning = QtGui.QTextEdit()
|
||||||
import FreeCADGui
|
self.lbl_warning.setReadOnly(True)
|
||||||
# TODO: use a mutex wrapping the entire method, if possible
|
self.lbl_warning.setAlignment(QtCore.Qt.AlignTop)
|
||||||
SafeViewer.enabled = True
|
self.lbl_warning.setText(
|
||||||
self.instance_enabled = True # Has this specific instance been enabled?
|
translate(
|
||||||
|
"SearchBar",
|
||||||
|
"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 App.",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.btn_enable_for_this_session = QtGui.QPushButton(
|
||||||
|
translate("SearchBar", "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(
|
||||||
|
translate("SearchBar", "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)
|
||||||
|
|
||||||
if (self.displaying_warning):
|
def enable_for_this_session(self):
|
||||||
self.layout().removeWidget(self.lbl_warning)
|
if not SafeViewer.enabled:
|
||||||
self.layout().removeWidget(self.btn_enable_for_this_session)
|
for instance in SafeViewer.instances:
|
||||||
self.layout().removeWidget(self.btn_enable_for_future_sessions)
|
instance.enable()
|
||||||
|
|
||||||
self.viewer = FreeCADGui.createViewer()
|
def enable_for_future_sessions(self):
|
||||||
self.graphicsView = self.viewer.graphicsView()
|
if not SafeViewer.enabled:
|
||||||
self.oldGraphicsViewParent = self.graphicsView.parent()
|
# Store in prefs
|
||||||
self.oldGraphicsViewParentParent = self.oldGraphicsViewParent.parent()
|
App.ParamGet("User parameter:BaseApp/Preferences/Mod/SearchBar").SetBool(
|
||||||
self.oldGraphicsViewParentParentParent = self.oldGraphicsViewParentParent.parent()
|
"PreviewEnabled", True
|
||||||
|
)
|
||||||
|
# Then enable as usual
|
||||||
|
self.enable_for_this_session()
|
||||||
|
|
||||||
# Avoid segfault but still hide the undesired window by moving it to a new hidden MDI area.
|
def enable(self):
|
||||||
self.hiddenQMDIArea = QtGui.QMdiArea()
|
if not self.instance_enabled:
|
||||||
self.hiddenQMDIArea.addSubWindow(self.oldGraphicsViewParentParentParent)
|
import FreeCADGui
|
||||||
|
|
||||||
self.private_widget = self.oldGraphicsViewParent
|
# TODO: use a mutex wrapping the entire method, if possible
|
||||||
self.private_widget.setParent(self.init_parent)
|
SafeViewer.enabled = True
|
||||||
|
self.instance_enabled = True # Has this specific instance been enabled?
|
||||||
|
|
||||||
self.setLayout(QtGui.QVBoxLayout())
|
if self.displaying_warning:
|
||||||
self.layout().addWidget(self.private_widget)
|
self.layout().removeWidget(self.lbl_warning)
|
||||||
self.layout().setContentsMargins(0,0,0,0)
|
self.layout().removeWidget(self.btn_enable_for_this_session)
|
||||||
|
self.layout().removeWidget(self.btn_enable_for_future_sessions)
|
||||||
|
|
||||||
def fin(slf):
|
self.viewer = FreeCADGui.createViewer()
|
||||||
slf.finalizer()
|
self.graphicsView = self.viewer.graphicsView()
|
||||||
|
self.oldGraphicsViewParent = self.graphicsView.parent()
|
||||||
|
self.oldGraphicsViewParentParent = self.oldGraphicsViewParent.parent()
|
||||||
|
self.oldGraphicsViewParentParentParent = (
|
||||||
|
self.oldGraphicsViewParentParent.parent()
|
||||||
|
)
|
||||||
|
|
||||||
import weakref
|
# Avoid segfault but still hide the undesired window by moving it to a new hidden MDI area.
|
||||||
weakref.finalize(self, fin, self)
|
self.hiddenQMDIArea = QtGui.QMdiArea()
|
||||||
|
self.hiddenQMDIArea.addSubWindow(self.oldGraphicsViewParentParentParent)
|
||||||
|
|
||||||
self.destroyed.connect(self.finalizer)
|
self.private_widget = self.oldGraphicsViewParent
|
||||||
|
self.private_widget.setParent(self.init_parent)
|
||||||
|
|
||||||
def finalizer(self):
|
self.setLayout(QtGui.QVBoxLayout())
|
||||||
# Cleanup in an order that doesn't cause a segfault:
|
self.layout().addWidget(self.private_widget)
|
||||||
if SafeViewer.enabled:
|
self.layout().setContentsMargins(0, 0, 0, 0)
|
||||||
self.private_widget.setParent(self.oldGraphicsViewParentParent)
|
|
||||||
self.oldGraphicsViewParentParentParent.close()
|
def fin(slf):
|
||||||
self.oldGraphicsViewParentParentParent = None
|
slf.finalizer()
|
||||||
self.oldGraphicsViewParentParent = None
|
|
||||||
self.oldGraphicsViewParent = None
|
import weakref
|
||||||
self.graphicsView = None
|
|
||||||
self.viewer = None
|
weakref.finalize(self, fin, self)
|
||||||
#self.parent = None
|
|
||||||
self.init_parent = None
|
self.destroyed.connect(self.finalizer)
|
||||||
self.hiddenQMDIArea = None
|
|
||||||
|
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:
|
# Example use:
|
||||||
|
|
839
SearchBox.py
|
@ -1,372 +1,543 @@
|
||||||
|
import FreeCAD as App
|
||||||
|
import FreeCADGui as Gui
|
||||||
import os
|
import os
|
||||||
from PySide import QtGui
|
|
||||||
from PySide import QtCore
|
from PySide.QtCore import (
|
||||||
import FreeCADGui # just used for FreeCADGui.updateGui()
|
Qt,
|
||||||
|
SIGNAL,
|
||||||
|
QSize,
|
||||||
|
QIdentityProxyModel,
|
||||||
|
QPoint,
|
||||||
|
)
|
||||||
|
from PySide.QtWidgets import (
|
||||||
|
QTabWidget,
|
||||||
|
QSlider,
|
||||||
|
QSpinBox,
|
||||||
|
QCheckBox,
|
||||||
|
QComboBox,
|
||||||
|
QLabel,
|
||||||
|
QTabWidget,
|
||||||
|
QSizePolicy,
|
||||||
|
QPushButton,
|
||||||
|
QLineEdit,
|
||||||
|
QTextEdit,
|
||||||
|
QListView,
|
||||||
|
QAbstractItemView,
|
||||||
|
QWidget,
|
||||||
|
QVBoxLayout,
|
||||||
|
QApplication,
|
||||||
|
QListWidget,
|
||||||
|
QWidgetAction,
|
||||||
|
)
|
||||||
|
from PySide.QtGui import (
|
||||||
|
QIcon,
|
||||||
|
QPixmap,
|
||||||
|
QColor,
|
||||||
|
QStandardItemModel,
|
||||||
|
QShortcut,
|
||||||
|
QKeySequence,
|
||||||
|
QStandardItem,
|
||||||
|
)
|
||||||
|
|
||||||
from SearchBoxLight import SearchBoxLight
|
from SearchBoxLight import SearchBoxLight
|
||||||
|
import Parameters_SearchBar as Parameters
|
||||||
|
|
||||||
|
genericToolIcon = QIcon(Parameters.genericToolIcon_Pixmap)
|
||||||
|
|
||||||
globalIgnoreFocusOut = False
|
globalIgnoreFocusOut = False
|
||||||
|
|
||||||
genericToolIcon = QtGui.QIcon(QtGui.QIcon(os.path.dirname(__file__) + '/Tango-Tools-spanner-hammer.svg'))
|
# Define the translation
|
||||||
|
translate = App.Qt.translate
|
||||||
|
|
||||||
|
# Avoid garbage collection by storing the action in a global variable
|
||||||
|
wax = None
|
||||||
|
sea = None
|
||||||
|
tbr = None
|
||||||
|
|
||||||
|
|
||||||
def easyToolTipWidget(html):
|
def easyToolTipWidget(html):
|
||||||
foo = QtGui.QTextEdit()
|
foo = QTextEdit()
|
||||||
foo.setReadOnly(True)
|
foo.setReadOnly(True)
|
||||||
foo.setAlignment(QtCore.Qt.AlignTop)
|
foo.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||||
foo.setText(html)
|
foo.setText(html)
|
||||||
return foo
|
return foo
|
||||||
|
|
||||||
class SearchBox(QtGui.QLineEdit):
|
|
||||||
# The following block of code is present in the lightweight proxy SearchBoxLight
|
def SearchBoxFunction(mw):
|
||||||
'''
|
import SearchBoxLight
|
||||||
resultSelected = QtCore.Signal(int, int)
|
|
||||||
'''
|
global wax, sea, tbr
|
||||||
@staticmethod
|
|
||||||
def lazyInit(self):
|
if mw:
|
||||||
if self.isInitialized:
|
if sea is None:
|
||||||
return self
|
sea = SearchBoxLight.SearchBoxLight(
|
||||||
getItemGroups = self.getItemGroups
|
getItemGroups=lambda: __import__("GetItemGroups").getItemGroups(),
|
||||||
getToolTip = self.getToolTip
|
getToolTip=lambda groupId, setParent: __import__("GetItemGroups").getToolTip(groupId, setParent),
|
||||||
getItemDelegate = self.getItemDelegate
|
getItemDelegate=lambda: __import__("IndentedItemDelegate").IndentedItemDelegate(),
|
||||||
maxVisibleRows = self.maxVisibleRows
|
)
|
||||||
# The following block of code is executed by the lightweight proxy SearchBoxLight
|
sea.resultSelected.connect(
|
||||||
'''
|
lambda index, groupId: __import__("GetItemGroups").onResultSelected(index, groupId)
|
||||||
|
)
|
||||||
|
|
||||||
|
if wax is None:
|
||||||
|
wax = QWidgetAction(None)
|
||||||
|
wax.setWhatsThis(
|
||||||
|
translate(
|
||||||
|
"SearchBar",
|
||||||
|
"Use this search bar to find tools, document objects, preferences and more",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
sea.setWhatsThis(
|
||||||
|
translate(
|
||||||
|
"SearchBar",
|
||||||
|
"Use this search bar to find tools, document objects, preferences and more",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
wax.setDefaultWidget(sea)
|
||||||
|
return wax
|
||||||
|
|
||||||
|
|
||||||
|
class SearchBox(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
|
||||||
|
"""
|
||||||
# Call parent constructor
|
# Call parent constructor
|
||||||
super(SearchBoxLight, self).__init__(parent)
|
super(SearchBoxLight, self).__init__(parent)
|
||||||
# Connect signals and slots
|
# Connect signals and slots
|
||||||
self.textChanged.connect(self.filterModel)
|
self.textChanged.connect(self.filterModel)
|
||||||
# Thanks to https://saurabhg.com/programming/search-box-using-qlineedit/ for indicating a few useful options
|
# Thanks to https://saurabhg.com/programming/search-box-using-qlineedit/ for indicating a few useful options
|
||||||
ico = QtGui.QIcon(':/icons/help-browser.svg')
|
ico = QIcon(':/icons/help-browser.svg')
|
||||||
#ico = QtGui.QIcon(':/icons/WhatsThis.svg')
|
#ico = QIcon(':/icons/WhatsThis.svg')
|
||||||
self.addAction(ico, QtGui.QLineEdit.LeadingPosition)
|
self.addAction(ico, QtGui.QLineEdit.LeadingPosition)
|
||||||
self.setClearButtonEnabled(True)
|
self.setClearButtonEnabled(True)
|
||||||
self.setPlaceholderText('Search tools, prefs & tree')
|
self.setPlaceholderText('Search tools, prefs & tree')
|
||||||
self.setFixedWidth(200) # needed to avoid a change of width when the clear button appears/disappears
|
self.setFixedWidth(200) # needed to avoid a change of width when the clear button appears/disappears
|
||||||
'''
|
"""
|
||||||
|
|
||||||
# Save arguments
|
# Save arguments
|
||||||
#self.model = model
|
# self.model = model
|
||||||
self.getItemGroups = getItemGroups
|
self.getItemGroups = getItemGroups
|
||||||
self.getToolTip = getToolTip
|
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.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
|
self.maxVisibleRows = maxVisibleRows # TODO: use this to compute the correct height
|
||||||
# Create proxy model
|
# Create proxy model
|
||||||
self.proxyModel = QtCore.QIdentityProxyModel()
|
self.proxyModel = 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.
|
# 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.mdl = QStandardItemModel()
|
||||||
#self.proxyModel.setModel(self.model)
|
# self.proxyModel.setModel(self.model)
|
||||||
# Create list view
|
# Create list view
|
||||||
self.listView = QtGui.QListView(self)
|
self.listView = QListView(self)
|
||||||
self.listView.setWindowFlags(QtGui.Qt.ToolTip)
|
self.listView.setWindowFlags(Qt.WindowType.ToolTip)
|
||||||
self.listView.setWindowFlag(QtGui.Qt.FramelessWindowHint)
|
self.listView.setWindowFlag(Qt.WindowType.FramelessWindowHint)
|
||||||
self.listView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
|
self.listView.setSelectionMode(self.listView.SelectionMode.SingleSelection)
|
||||||
self.listView.setModel(self.proxyModel)
|
self.listView.setModel(self.proxyModel)
|
||||||
self.listView.setItemDelegate(getItemDelegate()) # https://stackoverflow.com/a/65930408/324969
|
self.listView.setItemDelegate(getItemDelegate()) # https://stackoverflow.com/a/65930408/324969
|
||||||
# make the QListView non-editable
|
self.listView.setMouseTracking(True)
|
||||||
self.listView.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
|
# make the QListView non-editable
|
||||||
# Create pane for showing extra info about the currently-selected tool
|
self.listView.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||||
#self.extraInfo = QtGui.QLabel()
|
# Create pane for showing extra info about the currently-selected tool
|
||||||
self.extraInfo = QtGui.QWidget()
|
# self.extraInfo = QtGui.QLabel()
|
||||||
self.extraInfo.setWindowFlags(QtGui.Qt.ToolTip)
|
self.extraInfo = QWidget()
|
||||||
self.extraInfo.setWindowFlag(QtGui.Qt.FramelessWindowHint)
|
self.extraInfo.setWindowFlags(Qt.WindowType.ToolTip)
|
||||||
self.extraInfo.setLayout(QtGui.QVBoxLayout())
|
self.extraInfo.setWindowFlag(Qt.WindowType.FramelessWindowHint)
|
||||||
self.extraInfo.layout().setContentsMargins(0,0,0,0)
|
self.extraInfo.setLayout(QVBoxLayout())
|
||||||
self.setExtraInfoIsActive = False
|
self.extraInfo.layout().setContentsMargins(0, 0, 0, 0)
|
||||||
self.pendingExtraInfo = None
|
self.setExtraInfoIsActive = False
|
||||||
self.currentExtraInfo = None
|
self.pendingExtraInfo = None
|
||||||
# Connect signals and slots
|
self.currentExtraInfo = None
|
||||||
self.listView.clicked.connect(lambda x: self.selectResult('select', x))
|
# Connect signals and slots
|
||||||
self.listView.selectionModel().selectionChanged.connect(self.onSelectionChanged)
|
self.listView.clicked.connect(
|
||||||
|
lambda x: self.selectResult("select", x)
|
||||||
|
) # This makes all workbenches appear. TODO: findout why, a click event seems not logic
|
||||||
|
self.listView.selectionModel().selectionChanged.connect(
|
||||||
|
self.onSelectionChanged
|
||||||
|
) # This updates the details when using the keyboard
|
||||||
|
# Add custom mouse events. On windows the click events were not working for Searcbar versions 1.2.x and older.
|
||||||
|
# These events and their proxies in the SearchBorLight fixes this
|
||||||
|
self.listView.mousePressEvent = lambda event: self.proxyMousePressEvent(event)
|
||||||
|
self.listView.mouseMoveEvent = lambda event: self.proxyMouseMoveEvent(event)
|
||||||
|
self.extraInfo.leaveEvent = lambda event: self.proxyLeaveEvent(event)
|
||||||
|
|
||||||
# Note: should probably use the eventFilter method instead...
|
# Note: should probably use the eventFilter method instead...
|
||||||
wdgctx = QtCore.Qt.ShortcutContext.WidgetShortcut
|
wdgctx = Qt.ShortcutContext.WidgetShortcut
|
||||||
|
|
||||||
QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Down), self, context = wdgctx).activated.connect(self.listDown)
|
QShortcut(QKeySequence(Qt.Key.Key_Down), self, context=wdgctx).activated.connect(self.listDown)
|
||||||
QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Up), self, context = wdgctx).activated.connect(self.listUp)
|
QShortcut(QKeySequence(Qt.Key.Key_Up), self, context=wdgctx).activated.connect(self.listUp)
|
||||||
QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_PageDown), self, context = wdgctx).activated.connect(self.listPageDown)
|
QShortcut(QKeySequence(Qt.Key.Key_PageDown), self, context=wdgctx).activated.connect(self.listPageDown)
|
||||||
QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_PageUp), self, context = wdgctx).activated.connect(self.listPageUp)
|
QShortcut(QKeySequence(Qt.Key.Key_PageUp), self, context=wdgctx).activated.connect(self.listPageUp)
|
||||||
|
|
||||||
# Home and End do not work, for some reason.
|
# Home and End do not work, for some reason.
|
||||||
#QtGui.QShortcut(QtGui.QKeySequence.MoveToEndOfDocument, self, context = wdgctx).activated.connect(self.listEnd)
|
# QShortcut(QKeySequence.MoveToEndOfDocument, self, context = wdgctx).activated.connect(self.listEnd)
|
||||||
#QtGui.QShortcut(QtGui.QKeySequence.MoveToStartOfDocument, self, context = wdgctx).activated.connect(self.listStart)
|
# QShortcut(QKeySequence.MoveToStartOfDocument, self, context = wdgctx).activated.connect(self.listStart)
|
||||||
#QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_End), self, context = wdgctx).activated.connect(self.listEnd)
|
# QShortcut(QKeySequence(Qt.Key.Key_End), self, context = wdgctx).activated.connect(self.listEnd)
|
||||||
#QtGui.QShortcut(QtGui.QKeySequence('Home'), self, context = wdgctx).activated.connect(self.listStart)
|
# QShortcut(QKeySequence('Home'), self, context = wdgctx).activated.connect(self.listStart)
|
||||||
|
|
||||||
QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Enter), self, context = wdgctx).activated.connect(self.listAccept)
|
QShortcut(QKeySequence(Qt.Key.Key_Enter), self, context=wdgctx).activated.connect(self.listAccept)
|
||||||
QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Return), self, context = wdgctx).activated.connect(self.listAccept)
|
QShortcut(QKeySequence(Qt.Key.Key_Return), self, context=wdgctx).activated.connect(self.listAccept)
|
||||||
QtGui.QShortcut(QtGui.QKeySequence('Ctrl+Return'), self, context = wdgctx).activated.connect(self.listAcceptToggle)
|
QShortcut(QKeySequence("Ctrl+Return"), self, context=wdgctx).activated.connect(self.listAcceptToggle)
|
||||||
QtGui.QShortcut(QtGui.QKeySequence('Ctrl+Enter'), self, context = wdgctx).activated.connect(self.listAcceptToggle)
|
QShortcut(QKeySequence("Ctrl+Enter"), self, context=wdgctx).activated.connect(self.listAcceptToggle)
|
||||||
QtGui.QShortcut(QtGui.QKeySequence('Ctrl+Space'), self, context = wdgctx).activated.connect(self.listAcceptToggle)
|
QShortcut(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)
|
QShortcut(QKeySequence(Qt.Key.Key_Escape), self, context=wdgctx).activated.connect(self.listCancel)
|
||||||
|
|
||||||
# Initialize the model with the full list (assuming the text() is empty)
|
# 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.proxyFilterModel(self.text()) # This is done by refreshItemGroups on focusInEvent, because the initial loading from cache can take time
|
||||||
self.firstShowList = True
|
self.firstShowList = True
|
||||||
self.isInitialized = True
|
self.isInitialized = True
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def refreshItemGroups(self):
|
def proxyMousePressEvent(self, event):
|
||||||
self.itemGroups = self.getItemGroups()
|
if self.listView.underMouse():
|
||||||
self.proxyFilterModel(self.text())
|
self.selectResult(mode=None, index=self.listView.currentIndex())
|
||||||
|
else:
|
||||||
|
event.ignore()
|
||||||
|
return
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def proxyFocusInEvent(self, qFocusEvent):
|
def proxyMouseMoveEvent(self, arg__1):
|
||||||
if self.firstShowList:
|
index = self.listView.indexAt(arg__1.pos())
|
||||||
mdl = QtGui.QStandardItemModel()
|
self.listView.setCurrentIndex(index)
|
||||||
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
|
self.setExtraInfo(index)
|
||||||
def proxyFocusOutEvent(self, qFocusEvent):
|
# Poor attempt to circumvent a glitch where the extra info pane stays visible after pressing Return
|
||||||
global globalIgnoreFocusOut
|
if not self.listView.isHidden():
|
||||||
if not globalIgnoreFocusOut:
|
self.showExtraInfo()
|
||||||
self.hideList()
|
if self.listView.isHidden():
|
||||||
super(SearchBoxLight, self).focusOutEvent(qFocusEvent)
|
self.hideExtraInfo()
|
||||||
|
return
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def movementKey(self, rowUpdate):
|
def refreshItemGroups(self):
|
||||||
currentIndex = self.listView.currentIndex()
|
self.itemGroups = self.getItemGroups()
|
||||||
self.showList()
|
self.proxyFilterModel(self.text())
|
||||||
if self.listView.isEnabled():
|
|
||||||
currentRow = currentIndex.row()
|
@staticmethod
|
||||||
|
def proxyFocusInEvent(self, qFocusEvent):
|
||||||
|
# if the extrainfo is under the cursor, don't focus but only show the list
|
||||||
|
if self.extraInfo.underMouse():
|
||||||
|
self.showList()
|
||||||
|
qFocusEvent.ignore()
|
||||||
|
return
|
||||||
|
if self.firstShowList:
|
||||||
|
mdl = QStandardItemModel()
|
||||||
|
mdl.appendRow(
|
||||||
|
[
|
||||||
|
QStandardItem(
|
||||||
|
genericToolIcon,
|
||||||
|
translate("SearchBar", "Please wait, loading results from cache…"),
|
||||||
|
),
|
||||||
|
QStandardItem("0"),
|
||||||
|
QStandardItem("-1"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.proxyModel.setSourceModel(mdl)
|
||||||
|
self.showList()
|
||||||
|
self.firstShowList = False
|
||||||
|
Gui.updateGui()
|
||||||
|
global globalIgnoreFocusOut
|
||||||
|
if not globalIgnoreFocusOut:
|
||||||
|
self.refreshItemGroups()
|
||||||
|
self.showList()
|
||||||
|
super(SearchBoxLight, self).focusInEvent(qFocusEvent)
|
||||||
|
return
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def proxyFocusOutEvent(self, qFocusEvent):
|
||||||
|
global globalIgnoreFocusOut
|
||||||
|
if not globalIgnoreFocusOut:
|
||||||
|
self.hideList()
|
||||||
|
# super(SearchBoxLight, self).focusOutEvent(qFocusEvent)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def proxyLeaveEvent(self, qFocusEvent):
|
||||||
|
self.clearFocus()
|
||||||
|
self.hideList()
|
||||||
|
return
|
||||||
|
|
||||||
|
@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 == Qt.Key.Key_Home and modifiers & Qt.Key.CTRL != 0:
|
||||||
|
self.listStart()
|
||||||
|
elif key == Qt.Key.Key_End and modifiers & Qt.Key.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: None, 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:
|
||||||
|
# self.index = 0
|
||||||
|
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 = 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(
|
||||||
|
[
|
||||||
|
QStandardItem(group["icon"] or genericToolIcon, group["text"]),
|
||||||
|
QStandardItem(str(depth)),
|
||||||
|
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
|
||||||
|
indexSelect = 1
|
||||||
nbRows = self.listView.model().rowCount()
|
nbRows = self.listView.model().rowCount()
|
||||||
if nbRows > 0:
|
if nbRows > 0:
|
||||||
newRow = rowUpdate(currentRow, nbRows)
|
index = self.listView.model().index(indexSelect, 0)
|
||||||
index = self.listView.model().index(newRow, 0)
|
self.listView.setCurrentIndex(index)
|
||||||
self.listView.setCurrentIndex(index)
|
self.setExtraInfo(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:
|
else:
|
||||||
return None
|
self.clearExtraInfo()
|
||||||
def filterGroups(groups):
|
# self.showList()
|
||||||
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()
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def setFloatingWidgetsGeometry(self):
|
def setFloatingWidgetsGeometry(self):
|
||||||
def getScreenPosition(widget):
|
def getScreenPosition(widget):
|
||||||
geo = widget.geometry()
|
geo = widget.geometry()
|
||||||
parent = widget.parent()
|
parent = widget.parent()
|
||||||
parentPos = getScreenPosition(parent) if parent is not None else QtCore.QPoint(0,0)
|
parentPos = getScreenPosition(parent) if parent is not None else QPoint(0, 0)
|
||||||
return QtCore.QPoint(geo.x() + parentPos.x(), geo.y() + parentPos.y())
|
return QPoint(geo.x() + parentPos.x(), geo.y() + parentPos.y())
|
||||||
pos = getScreenPosition(self)
|
|
||||||
siz = self.size()
|
pos = getScreenPosition(self)
|
||||||
screen = QtGui.QGuiApplication.screenAt(pos)
|
siz = self.size()
|
||||||
x = pos.x()
|
screen = QApplication.screenAt(pos)
|
||||||
y = pos.y() + siz.height()
|
x = pos.x()
|
||||||
hint_w = self.listView.sizeHint().width()
|
y = pos.y() + siz.height()
|
||||||
# TODO: this can still bump into the bottom of the screen, in that case we should flip
|
hint_w = self.listView.sizeHint().width()
|
||||||
w = max(siz.width(), hint_w)
|
# TODO: this can still bump into the bottom of the screen, in that case we should flip
|
||||||
h = 200 # TODO: set height / size here according to desired number of items
|
w = max(siz.width(), hint_w)
|
||||||
extraw = w # choose a preferred width that doesn't change all the time,
|
h = 200 # TODO: set height / size here according to desired number of items
|
||||||
# self.extraInfo.sizeHint().width() would change for every item.
|
extraw = w # choose a preferred width that doesn't change all the time,
|
||||||
extrax = x - extraw
|
# self.extraInfo.sizeHint().width() would change for every item.
|
||||||
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
|
extrax = x - extraw
|
||||||
extraw = min(extraleftw, extraw)
|
if screen is not None:
|
||||||
self.listView.setGeometry(x, y, w, h)
|
scr = screen.geometry()
|
||||||
self.extraInfo.setGeometry(extrax, y, extraw, h)
|
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
|
@staticmethod
|
||||||
def proxyOnSelectionChanged(self, selected, deselected):
|
def proxyOnSelectionChanged(self, selected, deselected):
|
||||||
# The list has .setSelectionMode(QtGui.QAbstractItemView.SingleSelection),
|
# The list has .setSelectionMode(QAbstractItemView.SingleSelection),
|
||||||
# so there is always at most one index in selected.indexes() and at most one
|
# so there is always at most one index in selected.indexes() and at most one
|
||||||
# index in deselected.indexes()
|
# index in deselected.indexes()
|
||||||
selected = selected.indexes()
|
selected = selected.indexes()
|
||||||
deselected = deselected.indexes()
|
deselected = deselected.indexes()
|
||||||
if len(selected) > 0:
|
if len(selected) > 0:
|
||||||
index = selected[0]
|
index = selected[0]
|
||||||
self.setExtraInfo(index)
|
self.setExtraInfo(index)
|
||||||
# Poor attempt to circumvent a glitch where the extra info pane stays visible after pressing Return
|
# Poor attempt to circumvent a glitch where the extra info pane stays visible after pressing Return
|
||||||
if not self.listView.isHidden():
|
if not self.listView.isHidden():
|
||||||
self.showExtraInfo()
|
self.showExtraInfo()
|
||||||
elif len(deselected) > 0:
|
elif len(deselected) > 0:
|
||||||
self.hideExtraInfo()
|
self.hideExtraInfo()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def setExtraInfo(self, index):
|
def setExtraInfo(self, index):
|
||||||
if self.currentExtraInfo == (index.row(), index.column(), index.model()):
|
if self.currentExtraInfo == (index.row(), index.column(), index.model()):
|
||||||
# avoid useless updates of the extra info window; this also prevents segfaults when the widget
|
# 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
|
# is replaced when selecting an option from the right-click context menu
|
||||||
return
|
return
|
||||||
self.currentExtraInfo = (index.row(), index.column(), index.model())
|
self.currentExtraInfo = (index.row(), index.column(), index.model())
|
||||||
# TODO: use an atomic swap or mutex if possible
|
# TODO: use an atomic swap or mutex if possible
|
||||||
if self.setExtraInfoIsActive:
|
if self.setExtraInfoIsActive:
|
||||||
self.pendingExtraInfo = index
|
self.pendingExtraInfo = index
|
||||||
#print("boom")
|
# 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
|
|
||||||
else:
|
else:
|
||||||
break
|
self.setExtraInfoIsActive = True
|
||||||
#print("unlock")
|
# print("lock")
|
||||||
self.setExtraInfoIsActive = False
|
# 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 setParent(toolTipWidget):
|
||||||
def clearExtraInfo(self):
|
nonlocal parentIsSet
|
||||||
# TODO: just clear the contents but keep the widget visible.
|
parentIsSet = True
|
||||||
self.extraInfo.hide()
|
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
|
toolTipWidget = self.getToolTip(groupId, setParent)
|
||||||
def showExtraInfo(self):
|
if isinstance(toolTipWidget, str):
|
||||||
self.extraInfo.show()
|
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,97 @@
|
||||||
from PySide import QtGui
|
from PySide import QtGui
|
||||||
from PySide import QtCore
|
from PySide import QtCore
|
||||||
|
|
||||||
|
|
||||||
# This is a "light" version of the SearchBox implementation, which loads the actual implementation on first click
|
# This is a "light" version of the SearchBox implementation, which loads the actual implementation on first click
|
||||||
class SearchBoxLight(QtGui.QLineEdit):
|
class SearchBoxLight(QtGui.QLineEdit):
|
||||||
resultSelected = QtCore.Signal(int, int)
|
resultSelected = QtCore.Signal(int, int)
|
||||||
def __init__(self, getItemGroups, getToolTip, getItemDelegate, maxVisibleRows = 20, parent = None):
|
|
||||||
self.isInitialized = False
|
|
||||||
|
|
||||||
# Store arguments
|
def __init__(
|
||||||
self.getItemGroups = getItemGroups
|
self, getItemGroups, getToolTip, getItemDelegate, maxVisibleRows=20, parent=None
|
||||||
self.getToolTip = getToolTip
|
):
|
||||||
self.getItemDelegate = getItemDelegate
|
self.isInitialized = False
|
||||||
self.maxVisibleRows = maxVisibleRows
|
|
||||||
|
|
||||||
# Call parent constructor
|
# Store arguments
|
||||||
super(SearchBoxLight, self).__init__(parent)
|
self.getItemGroups = getItemGroups
|
||||||
# Connect signals and slots
|
self.getToolTip = getToolTip
|
||||||
self.textChanged.connect(self.filterModel)
|
self.getItemDelegate = getItemDelegate
|
||||||
# Thanks to https://saurabhg.com/programming/search-box-using-qlineedit/ for indicating a few useful options
|
self.maxVisibleRows = maxVisibleRows
|
||||||
ico = QtGui.QIcon(':/icons/help-browser.svg')
|
|
||||||
#ico = QtGui.QIcon(':/icons/WhatsThis.svg')
|
# Call parent constructor
|
||||||
self.addAction(ico, QtGui.QLineEdit.LeadingPosition)
|
super(SearchBoxLight, self).__init__(parent)
|
||||||
self.setClearButtonEnabled(True)
|
# Connect signals and slots
|
||||||
self.setPlaceholderText('Search tools, prefs & tree')
|
self.textChanged.connect(self.filterModel)
|
||||||
self.setFixedWidth(200) # needed to avoid a change of width when the clear button appears/disappears
|
# Thanks to https://saurabhg.com/programming/search-box-using-qlineedit/ for indicating a few useful options
|
||||||
def lazyInit(self):
|
ico = QtGui.QIcon(":/icons/help-browser.svg")
|
||||||
pass
|
# ico = QtGui.QIcon(':/icons/WhatsThis.svg')
|
||||||
def __getattr__(self, name):
|
self.addAction(ico, QtGui.QLineEdit.LeadingPosition)
|
||||||
import types
|
self.setClearButtonEnabled(True)
|
||||||
def f(*args, **kwargs):
|
self.setPlaceholderText("Search tools, prefs & tree")
|
||||||
import SearchBox
|
self.setFixedWidth(
|
||||||
SearchBox.SearchBox.lazyInit(self)
|
200
|
||||||
return getattr(SearchBox.SearchBox, name)(*args, **kwargs)
|
) # needed to avoid a change of width when the clear button appears/disappears
|
||||||
return types.MethodType(f, self)
|
|
||||||
def focusInEvent(self, *args, **kwargs): return self.proxyFocusInEvent(*args, **kwargs)
|
def lazyInit(self):
|
||||||
def focusOutEvent(self, *args, **kwargs): return self.proxyFocusOutEvent(*args, **kwargs)
|
pass
|
||||||
def keyPressEvent(self, *args, **kwargs): return self.proxyKeyPressEvent(*args, **kwargs)
|
|
||||||
def onSelectionChanged(self, *args, **kwargs): return self.proxyOnSelectionChanged(*args, **kwargs)
|
def __getattr__(self, name):
|
||||||
def filterModel(self, *args, **kwargs): return self.proxyFilterModel(*args, **kwargs)
|
import types
|
||||||
def listDown(self, *args, **kwargs): return self.proxyListDown(*args, **kwargs)
|
|
||||||
def listUp(self, *args, **kwargs): return self.proxyListUp(*args, **kwargs)
|
def f(*args, **kwargs):
|
||||||
def listPageDown(self, *args, **kwargs): return self.proxyListPageDown(*args, **kwargs)
|
import SearchBox
|
||||||
def listPageUp(self, *args, **kwargs): return self.proxyListPageUp(*args, **kwargs)
|
|
||||||
def listEnd(self, *args, **kwargs): return self.proxyListEnd(*args, **kwargs)
|
SearchBox.SearchBox.lazyInit(self)
|
||||||
def listStart(self, *args, **kwargs): return self.proxyListStart(*args, **kwargs)
|
return getattr(SearchBox.SearchBox, name)(*args, **kwargs)
|
||||||
def listAccept(self, *args, **kwargs): return self.proxyListAccept(*args, **kwargs)
|
|
||||||
def listAcceptToggle(self, *args, **kwargs): return self.proxyListAcceptToggle(*args, **kwargs)
|
return types.MethodType(f, self)
|
||||||
def listCancel(self, *args, **kwargs): return self.proxyListCancel(*args, **kwargs)
|
|
||||||
|
def MousePressEvent(self, *args, **kwargs):
|
||||||
|
return self.proxyMousePressEvent(*args, **kwargs)
|
||||||
|
|
||||||
|
def MouseMoveEvent(self, *args, **kwargs):
|
||||||
|
return self.proxyMouseMoveEvent(*args, **kwargs)
|
||||||
|
|
||||||
|
def LeaveEvent(self, *args, **kwargs):
|
||||||
|
return self.proxyLeaveEvent(*args, **kwargs)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
actionHandlers = { }
|
actionHandlers = {}
|
||||||
toolTipHandlers = { }
|
toolTipHandlers = {}
|
||||||
resultProvidersCached = { }
|
resultProvidersCached = {}
|
||||||
resultProvidersUncached = { }
|
resultProvidersUncached = {}
|
||||||
|
|
||||||
|
|
||||||
# name : string
|
# name : string
|
||||||
# getItemGroupsCached: () -> [itemGroup]
|
# getItemGroupsCached: () -> [itemGroup]
|
||||||
# getItemGroupsUncached: () -> [itemGroup]
|
# getItemGroupsUncached: () -> [itemGroup]
|
||||||
def registerResultProvider(name, getItemGroupsCached, getItemGroupsUncached):
|
def registerResultProvider(name, getItemGroupsCached, getItemGroupsUncached):
|
||||||
resultProvidersCached[name] = getItemGroupsCached
|
resultProvidersCached[name] = getItemGroupsCached
|
||||||
resultProvidersUncached[name] = getItemGroupsUncached
|
resultProvidersUncached[name] = getItemGroupsUncached
|
||||||
|
|
||||||
|
|
||||||
# name : str
|
# name : str
|
||||||
# action : act -> None
|
# action : act -> None
|
||||||
# toolTip : groupId, setParent -> (str or QWidget)
|
# toolTip : groupId, setParent -> (str or QWidget)
|
||||||
def registerResultHandler(name, action, toolTip):
|
def registerResultHandler(name, action, toolTip):
|
||||||
actionHandlers[name] = action
|
actionHandlers[name] = action
|
||||||
toolTipHandlers[name] = toolTip
|
toolTipHandlers[name] = toolTip
|
||||||
|
|
87
Serialize.py
|
@ -1,87 +0,0 @@
|
||||||
from PySide import QtCore
|
|
||||||
from PySide 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 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
|
|
||||||
|
|
||||||
# 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']),
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
def deserializeTool(tool):
|
|
||||||
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'])
|
|
||||||
}
|
|
||||||
|
|
||||||
def serializeItemGroups(itemGroups):
|
|
||||||
return [serializeItemGroup(itemGroup) for itemGroup in itemGroups]
|
|
||||||
|
|
||||||
def serialize(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'])
|
|
||||||
}
|
|
||||||
|
|
||||||
def deserializeItemGroups(serializedItemGroups):
|
|
||||||
return [deserializeItemGroup(itemGroup) for itemGroup in serializedItemGroups]
|
|
||||||
|
|
||||||
def deserialize(serializedItemGroups):
|
|
||||||
return deserializeItemGroups(json.loads(serializedItemGroups))
|
|
156
Serialize_SearchBar.py
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
from PySide import QtCore
|
||||||
|
from PySide import QtGui
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
def iconToBase64(
|
||||||
|
icon: QtGui.QIcon,
|
||||||
|
sz=QtCore.QSize(64, 64),
|
||||||
|
mode=QtGui.QIcon.Mode.Normal,
|
||||||
|
state=QtGui.QIcon.State.On,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Converts a QIcon to a Base64-encoded string representation of its pixmap.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
icon (QIcon): The icon to encode.
|
||||||
|
sz (QSize): The size of the pixmap to generate.
|
||||||
|
mode (QIcon.Mode): The mode of the pixmap (e.g., Normal, Disabled).
|
||||||
|
state (QIcon.State): The state of the pixmap (e.g., On, Off).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The Base64-encoded string of the icon's pixmap.
|
||||||
|
"""
|
||||||
|
buf = QtCore.QBuffer()
|
||||||
|
buf.open(QtCore.QIODevice.OpenModeFlag.WriteOnly)
|
||||||
|
|
||||||
|
# Save the pixmap of the icon to the buffer in PNG format
|
||||||
|
pixmap: QtGui.QPixmap = icon.pixmap(sz, mode, state)
|
||||||
|
try:
|
||||||
|
pixmap.save(buf, "PNG")
|
||||||
|
except Exception as e:
|
||||||
|
# raise ValueError("Failed to save icon to buffer. Ensure the icon is valid.")
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
# Use standard Base64 encoding
|
||||||
|
base64_data = buf.data().toBase64().data().decode("utf-8")
|
||||||
|
buf.close()
|
||||||
|
return base64_data
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
# 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"]),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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(
|
||||||
|
bytearray(statePixmap.encode("utf-8"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
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"]),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def serializeItemGroup(itemGroup):
|
||||||
|
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]
|
||||||
|
|
||||||
|
|
||||||
|
def serialize(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"]),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def deserializeItemGroups(serializedItemGroups):
|
||||||
|
return [deserializeItemGroup(itemGroup) for itemGroup in serializedItemGroups]
|
||||||
|
|
||||||
|
|
||||||
|
def deserialize(serializedItemGroups):
|
||||||
|
return deserializeItemGroups(json.loads(serializedItemGroups))
|
239
StyleMapping_SearchBar.py
Normal file
|
@ -0,0 +1,239 @@
|
||||||
|
# *************************************************************************
|
||||||
|
# * *
|
||||||
|
# * Copyright (c) 2019-2024 Paul Ebbers *
|
||||||
|
# * *
|
||||||
|
# * This program is free software; you can redistribute it and/or modify *
|
||||||
|
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||||
|
# * as published by the Free Software Foundation; either version 3 of *
|
||||||
|
# * the License, or (at your option) any later version. *
|
||||||
|
# * for detail see the LICENCE text file. *
|
||||||
|
# * *
|
||||||
|
# * This program is distributed in the hope that it will be useful, *
|
||||||
|
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||||
|
# * GNU Library General Public License for more details. *
|
||||||
|
# * *
|
||||||
|
# * You should have received a copy of the GNU Library General Public *
|
||||||
|
# * License along with this program; if not, write to the Free Software *
|
||||||
|
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||||
|
# * USA *
|
||||||
|
# * *
|
||||||
|
# *************************************************************************
|
||||||
|
import FreeCAD as App
|
||||||
|
import FreeCADGui as Gui
|
||||||
|
import os
|
||||||
|
from PySide.QtGui import QIcon, QPixmap, QAction
|
||||||
|
from PySide.QtWidgets import (
|
||||||
|
QListWidgetItem,
|
||||||
|
QTableWidgetItem,
|
||||||
|
QListWidget,
|
||||||
|
QTableWidget,
|
||||||
|
QToolBar,
|
||||||
|
QToolButton,
|
||||||
|
QComboBox,
|
||||||
|
QPushButton,
|
||||||
|
QMenu,
|
||||||
|
QWidget,
|
||||||
|
QMainWindow,
|
||||||
|
)
|
||||||
|
from PySide.QtCore import Qt, SIGNAL, Signal, QObject, QThread
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
import shutil
|
||||||
|
import Standard_Functions_RIbbon as StandardFunctions
|
||||||
|
import Parameters_Ribbon
|
||||||
|
import webbrowser
|
||||||
|
import time
|
||||||
|
|
||||||
|
# Get the resources
|
||||||
|
pathIcons = Parameters_Ribbon.ICON_LOCATION
|
||||||
|
pathStylSheets = Parameters_Ribbon.STYLESHEET_LOCATION
|
||||||
|
pathUI = Parameters_Ribbon.UI_LOCATION
|
||||||
|
pathBackup = Parameters_Ribbon.BACKUP_LOCATION
|
||||||
|
sys.path.append(pathIcons)
|
||||||
|
sys.path.append(pathStylSheets)
|
||||||
|
sys.path.append(pathUI)
|
||||||
|
sys.path.append(pathBackup)
|
||||||
|
|
||||||
|
|
||||||
|
def ReturnStyleItem(ControlName, ShowCustomIcon=False, IgnoreOverlay=False):
|
||||||
|
"""
|
||||||
|
Enter one of the names below:
|
||||||
|
|
||||||
|
ControlName (string):
|
||||||
|
"Background_Color" returns string,
|
||||||
|
"Border_Color" returns string,
|
||||||
|
"FontColor" returns string,
|
||||||
|
"FontColor" returns string,
|
||||||
|
"""
|
||||||
|
# define a result holder and a dict for the StyleMapping file
|
||||||
|
result = "none"
|
||||||
|
|
||||||
|
# Get the current stylesheet for FreeCAD
|
||||||
|
FreeCAD_preferences = App.ParamGet("User parameter:BaseApp/Preferences/MainWindow")
|
||||||
|
currentStyleSheet = FreeCAD_preferences.GetString("StyleSheet")
|
||||||
|
IsInList = False
|
||||||
|
for key, value in StyleMapping_default["Stylesheets"].items():
|
||||||
|
if key == currentStyleSheet:
|
||||||
|
IsInList = True
|
||||||
|
break
|
||||||
|
if IsInList is False:
|
||||||
|
currentStyleSheet = "none"
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = StyleMapping_default["Stylesheets"][currentStyleSheet][ControlName]
|
||||||
|
if result == "" or result is None:
|
||||||
|
result = StyleMapping_default["Stylesheets"][""][ControlName]
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def ReturnColor(ColorType="Background_Color"):
|
||||||
|
mw: QMainWindow = Gui.getMainWindow()
|
||||||
|
palette = mw.style().standardPalette()
|
||||||
|
# Get the color
|
||||||
|
Color = palette.base().color().toTuple() # RGBA tupple
|
||||||
|
if ColorType == "Border_Color":
|
||||||
|
Color = palette.buttonText().color().toTuple()
|
||||||
|
if ColorType == "Background_Color_Hover":
|
||||||
|
Color = palette.highlight().color().toTuple()
|
||||||
|
|
||||||
|
HexColor = StandardFunctions.ColorConvertor(Color, Color[3] / 255, True, False)
|
||||||
|
|
||||||
|
return HexColor
|
||||||
|
|
||||||
|
|
||||||
|
def ReturnFontColor():
|
||||||
|
fontColor = "#000000"
|
||||||
|
IsDarkTheme = DarkMode()
|
||||||
|
|
||||||
|
if IsDarkTheme is True:
|
||||||
|
fontColor = "#ffffff"
|
||||||
|
|
||||||
|
return fontColor
|
||||||
|
|
||||||
|
|
||||||
|
def DarkMode():
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Define the standard result
|
||||||
|
IsDarkTheme = False
|
||||||
|
|
||||||
|
# Get the current stylesheet for FreeCAD
|
||||||
|
FreeCAD_preferences = App.ParamGet("User parameter:BaseApp/Preferences/MainWindow")
|
||||||
|
currentStyleSheet = FreeCAD_preferences.GetString("StyleSheet")
|
||||||
|
|
||||||
|
path = os.path.dirname(__file__)
|
||||||
|
# Get the folder with add-ons
|
||||||
|
for i in range(2):
|
||||||
|
# Starting point
|
||||||
|
path = os.path.dirname(path)
|
||||||
|
|
||||||
|
# Go through the sub-folders
|
||||||
|
for root, dirs, files in os.walk(path):
|
||||||
|
for name in dirs:
|
||||||
|
# if the current stylesheet matches a sub directory, try to geth the pacakgexml
|
||||||
|
if currentStyleSheet.replace(".qss", "").lower() in name.lower():
|
||||||
|
try:
|
||||||
|
packageXML = os.path.join(path, name, "package.xml")
|
||||||
|
|
||||||
|
# Get the tree and root of the xml file
|
||||||
|
tree = ET.parse(packageXML)
|
||||||
|
treeRoot = tree.getroot()
|
||||||
|
|
||||||
|
# Get all the tag elements
|
||||||
|
elements = []
|
||||||
|
namespaces = {"i": "https://wiki.freecad.org/Package_Metadata"}
|
||||||
|
elements = treeRoot.findall(".//i:content/i:preferencepack/i:tag", namespaces)
|
||||||
|
|
||||||
|
# go throug all tags. If 'dark' in the element text, this is a dark theme
|
||||||
|
for element in elements:
|
||||||
|
if "dark" in element.text.lower():
|
||||||
|
IsDarkTheme = True
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return IsDarkTheme
|
||||||
|
|
||||||
|
|
||||||
|
StyleMapping_default = {
|
||||||
|
"Stylesheets": {
|
||||||
|
"": {
|
||||||
|
"Background_Color": "#f0f0f0",
|
||||||
|
"Background_Color_Hover": "#ced4da",
|
||||||
|
"Border_Color": "#646464",
|
||||||
|
"FontColor": ReturnFontColor(),
|
||||||
|
},
|
||||||
|
"none": {
|
||||||
|
"Background_Color": "none",
|
||||||
|
"Background_Color_Hover": "#48a0f8",
|
||||||
|
"Border_Color": ReturnColor("Border_Color"),
|
||||||
|
"FontColor": ReturnFontColor(),
|
||||||
|
},
|
||||||
|
"FreeCAD Dark.qss": {
|
||||||
|
"Background_Color": "#333333",
|
||||||
|
"Background_Color_Hover": "#48a0f8",
|
||||||
|
"Border_Color": "#ffffff",
|
||||||
|
"FontColor": "#ffffff",
|
||||||
|
},
|
||||||
|
"FreeCAD Light.qss": {
|
||||||
|
"Background_Color": "#f0f0f0",
|
||||||
|
"Background_Color_Hover": "#48a0f8",
|
||||||
|
"Border_Color": "#646464",
|
||||||
|
"FontColor": "#000000",
|
||||||
|
},
|
||||||
|
"OpenLight.qss": {
|
||||||
|
"Background_Color": "#dee2e6",
|
||||||
|
"Background_Color_Hover": "#a5d8ff",
|
||||||
|
"Border_Color": "#1c7ed6",
|
||||||
|
"FontColor": "#000000",
|
||||||
|
},
|
||||||
|
"OpenDark.qss": {
|
||||||
|
"Background_Color": "#212529",
|
||||||
|
"Background_Color_Hover": "#1f364d",
|
||||||
|
"Border_Color": "#264b69",
|
||||||
|
"FontColor": "#ffffff",
|
||||||
|
},
|
||||||
|
"Behave-dark.qss": {
|
||||||
|
"Background_Color": "#232932",
|
||||||
|
"Background_Color_Hover": "#557bb6",
|
||||||
|
"Border_Color": "#3a7400",
|
||||||
|
"FontColor": ReturnFontColor(),
|
||||||
|
},
|
||||||
|
"ProDark.qss": {
|
||||||
|
"Background_Color": "#333333",
|
||||||
|
"Background_Color_Hover": "#557bb6",
|
||||||
|
"Border_Color": "#adc5ed",
|
||||||
|
"FontColor": ReturnFontColor(),
|
||||||
|
},
|
||||||
|
"Darker.qss": {
|
||||||
|
"Background_Color": "#444444",
|
||||||
|
"Background_Color_Hover": "#4aa5ff",
|
||||||
|
"Border_Color": "#696968",
|
||||||
|
"FontColor": ReturnFontColor(),
|
||||||
|
},
|
||||||
|
"Light-modern.qss": {
|
||||||
|
"Background_Color": "#f0f0f0",
|
||||||
|
"Background_Color_Hover": "#4aa5ff",
|
||||||
|
"Border_Color": "#646464",
|
||||||
|
"FontColor": ReturnFontColor(),
|
||||||
|
},
|
||||||
|
"Dark-modern.qss": {
|
||||||
|
"Background_Color": "#2b2b2b",
|
||||||
|
"Background_Color_Hover": "#4aa5ff",
|
||||||
|
"Border_Color": "#ffffff",
|
||||||
|
"FontColor": ReturnFontColor(),
|
||||||
|
},
|
||||||
|
"Dark-contrast.qss": {
|
||||||
|
"Background_Color": "#444444",
|
||||||
|
"Background_Color_Hover": "#4aa5ff",
|
||||||
|
"Border_Color": "#787878",
|
||||||
|
"FontColor": ReturnFontColor(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
26
package.xml
|
@ -1,28 +1,34 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
||||||
<package format="1" xmlns="https://wiki.freecad.org/Package_Metadata">
|
<package format="1" xmlns="https://wiki.freecad.org/Package_Metadata">
|
||||||
|
|
||||||
<name>SearchBar</name>
|
<name>SearchBar</name>
|
||||||
|
|
||||||
<description>Adds a search bar widget for tools, document objects, and preferences</description>
|
<description>Adds a search bar widget for tools, document objects, and preferences</description>
|
||||||
<version>1.0.2</version>
|
|
||||||
|
<version>1.4.1</version>
|
||||||
|
|
||||||
<date>2022-06-01</date>
|
<date>2022-06-01</date>
|
||||||
<author email="searchbar@suzanne.soy">Suzanne Soy</author>
|
|
||||||
<maintainer email="searchbar@suzanne.soy">Suzanne Soy</maintainer>
|
<maintainer>Paul Ebbers</maintainer>
|
||||||
|
|
||||||
<license file="LICENSE">CCOv1</license>
|
<license file="LICENSE">CCOv1</license>
|
||||||
<url type="repository" branch="main">https://github.com/SuzanneSoy/SearchBar</url>
|
|
||||||
<url type="bugtracker">https://github.com/SuzanneSoy/SearchBar/issues</url>
|
<url type="repository" branch="main">https://github.com/APEbbers/SearchBar</url>
|
||||||
<url type="documentation">https://github.com/SuzanneSoy/SearchBar</url>
|
|
||||||
<icon>Tango-System-search.svg</icon>
|
<url type="bugtracker">https://github.com/APEbbers/SearchBar/issues</url>
|
||||||
|
|
||||||
|
<url type="documentation">https://github.com/APEbbers/SearchBar</url>
|
||||||
|
|
||||||
<depend type="python">lxml</depend>
|
<depend type="python">lxml</depend>
|
||||||
<content>
|
<content>
|
||||||
<workbench>
|
<workbench>
|
||||||
<name>SearchBar</name>
|
<name>SearchBar</name>
|
||||||
<description>Adds a search bar widget for tools, document objects, and preferences</description>
|
|
||||||
<classname></classname>
|
|
||||||
<icon>Tango-System-search.svg</icon>
|
<icon>Tango-System-search.svg</icon>
|
||||||
<subdirectory>./</subdirectory>
|
<subdirectory>./</subdirectory>
|
||||||
<tag>search</tag>
|
<tag>search</tag>
|
||||||
<tag>widget</tag>
|
<tag>widget</tag>
|
||||||
<tag>ui/ux</tag>
|
<tag>ui/ux</tag>
|
||||||
<version>0.1.0</version>
|
|
||||||
</workbench>
|
</workbench>
|
||||||
</content>
|
</content>
|
||||||
|
|
||||||
</package>
|
</package>
|
||||||
|
|
104
translations/README.md
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
# About translating Ribbon Addon
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> All commands **must** be run in `./translations/` directory.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> If you want to update/release the files you need to have installed
|
||||||
|
> `lupdate` and `lrelease` from Qt6 version. Using the versions from
|
||||||
|
> Qt5 is not advised because they're buggy.
|
||||||
|
|
||||||
|
## Updating translations template file
|
||||||
|
|
||||||
|
To update the template file from source files you should use this command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
./update_translation.sh -U
|
||||||
|
```
|
||||||
|
|
||||||
|
Once done you can commit the changes and upload the new file to CrowdIn platform
|
||||||
|
at <https://crowdin.com/project/freecad-addons> webpage and find the **Ribbon** project.
|
||||||
|
|
||||||
|
## Creating file for missing locale
|
||||||
|
|
||||||
|
### Using script
|
||||||
|
|
||||||
|
To create a file for a new language with all **Ribbon** translatable strings execute
|
||||||
|
the script with `-u` flag plus your locale:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
./update_translation.sh -u de
|
||||||
|
```
|
||||||
|
|
||||||
|
### Renaming file
|
||||||
|
|
||||||
|
Also you can rename new `SearchBar.ts` file by appending the locale code,
|
||||||
|
for example, `SearchBar_de.ts` for German and change
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<TS version="2.1">
|
||||||
|
```
|
||||||
|
|
||||||
|
to
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<TS version="2.1" language="de" sourcelanguage="en">
|
||||||
|
```
|
||||||
|
|
||||||
|
As of 2024/10/28 the supported locales on FreeCAD
|
||||||
|
(according to `FreeCADGui.supportedLocales()`) are 44:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{'English': 'en', 'Afrikaans': 'af', 'Arabic': 'ar', 'Basque': 'eu',
|
||||||
|
'Belarusian': 'be', 'Bulgarian': 'bg', 'Catalan': 'ca',
|
||||||
|
'Chinese Simplified': 'zh-CN', 'Chinese Traditional': 'zh-TW', 'Croatian': 'hr',
|
||||||
|
'Czech': 'cs', 'Danish': 'da', 'Dutch': 'nl', 'Filipino': 'fil', 'Finnish': 'fi',
|
||||||
|
'French': 'fr', 'Galician': 'gl', 'Georgian': 'ka', 'German': 'de', 'Greek': 'el',
|
||||||
|
'Hungarian': 'hu', 'Indonesian': 'id', 'Italian': 'it', 'Japanese': 'ja',
|
||||||
|
'Kabyle': 'kab', 'Korean': 'ko', 'Lithuanian': 'lt', 'Norwegian': 'no',
|
||||||
|
'Polish': 'pl', 'Portuguese': 'pt-PT', 'Portuguese, Brazilian': 'pt-BR',
|
||||||
|
'Romanian': 'ro', 'Russian': 'ru', 'Serbian': 'sr', 'Serbian, Latin': 'sr-CS',
|
||||||
|
'Slovak': 'sk', 'Slovenian': 'sl', 'Spanish': 'es-ES', 'Spanish, Argentina': 'es-AR',
|
||||||
|
'Swedish': 'sv-SE', 'Turkish': 'tr', 'Ukrainian': 'uk', 'Valencian': 'val-ES',
|
||||||
|
'Vietnamese': 'vi'}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Translating
|
||||||
|
|
||||||
|
To edit your language file open your file in `Qt Linguist` from `qt5-tools`/`qt6-tools`
|
||||||
|
package or in a text editor like `xed`, `mousepad`, `gedit`, `nano`, `vim`/`nvim`,
|
||||||
|
`geany` etc. and translate it.
|
||||||
|
|
||||||
|
Alternatively you can visit the **FreeCAD-addons** project on CrowdIn platform
|
||||||
|
at <https://crowdin.com/project/freecad-addons> webpage and find your language,
|
||||||
|
once done, look for the **Ribbon** project.
|
||||||
|
|
||||||
|
## Compiling translations
|
||||||
|
|
||||||
|
To convert all `.ts` files to `.qm` files (merge) you can use this command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
./update_translation.sh -R
|
||||||
|
```
|
||||||
|
|
||||||
|
If you are a translator that wants to update only their language file
|
||||||
|
to test it on **FreeCAD** before doing a PR you can use this command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
./update_translation.sh -r de
|
||||||
|
```
|
||||||
|
|
||||||
|
This will update the `.qm` file for your language (German in this case).
|
||||||
|
|
||||||
|
## Sending translations
|
||||||
|
|
||||||
|
Now you can contribute your translated `.ts` file to **Ribbon** repository,
|
||||||
|
also include the `.qm` file.
|
||||||
|
|
||||||
|
<https://github.com/APEbbers/FreeCAD-Ribbon>
|
||||||
|
|
||||||
|
## More information
|
||||||
|
|
||||||
|
You can read more about translating external workbenches here:
|
||||||
|
|
||||||
|
<https://wiki.freecad.org/Translating_an_external_workbench>
|
168
translations/update_translation.sh
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# Create, update and release translation files.
|
||||||
|
#
|
||||||
|
# Supported locales on FreeCAD <2024-10-14, FreeCADGui.supportedLocales(), total=44>:
|
||||||
|
# {'English': 'en', 'Afrikaans': 'af', 'Arabic': 'ar', 'Basque': 'eu', 'Belarusian': 'be',
|
||||||
|
# 'Bulgarian': 'bg', 'Catalan': 'ca', 'Chinese Simplified': 'zh-CN',
|
||||||
|
# 'Chinese Traditional': 'zh-TW', 'Croatian': 'hr', 'Czech': 'cs', 'Danish': 'da',
|
||||||
|
# 'Dutch': 'nl', 'Filipino': 'fil', 'Finnish': 'fi', 'French': 'fr', 'Galician': 'gl',
|
||||||
|
# 'Georgian': 'ka', 'German': 'de', 'Greek': 'el', 'Hungarian': 'hu', 'Indonesian': 'id',
|
||||||
|
# 'Italian': 'it', 'Japanese': 'ja', 'Kabyle': 'kab', 'Korean': 'ko', 'Lithuanian': 'lt',
|
||||||
|
# 'Norwegian': 'no', 'Polish': 'pl', 'Portuguese': 'pt-PT', 'Portuguese, Brazilian': 'pt-BR',
|
||||||
|
# 'Romanian': 'ro', 'Russian': 'ru', 'Serbian': 'sr', 'Serbian, Latin': 'sr-CS', 'Slovak': 'sk',
|
||||||
|
# 'Slovenian': 'sl', 'Spanish': 'es-ES', 'Spanish, Argentina': 'es-AR', 'Swedish': 'sv-SE',
|
||||||
|
# 'Turkish': 'tr', 'Ukrainian': 'uk', 'Valencian': 'val-ES', 'Vietnamese': 'vi'}
|
||||||
|
#
|
||||||
|
# NOTE: PREPARATION
|
||||||
|
# - Install Qt tools
|
||||||
|
# Debian-based (e.g., Ubuntu): $ sudo apt-get install qttools5-dev-tools pyqt6-dev-tools
|
||||||
|
# Fedora-based: $ sudo dnf install qt6-linguist qt6-devel
|
||||||
|
# Arch-based: $ sudo pacman -S qt6-tools python-pyqt6
|
||||||
|
# - Make the script executable
|
||||||
|
# $ chmod +x update_translation.sh
|
||||||
|
# - The script has to be executed within the `translations` directory.
|
||||||
|
# Executing the script with no flags invokes the help.
|
||||||
|
# $ ./update_translation.sh
|
||||||
|
#
|
||||||
|
# NOTE: WORKFLOW TRANSLATOR (LOCAL)
|
||||||
|
# - Execute the script passing the `-u` flag plus locale code as argument
|
||||||
|
# Only update the file(s) you're translating!
|
||||||
|
# $ ./update_translation.sh -u es-ES
|
||||||
|
# - Do the translation via Qt Linguist and use `File>Release`
|
||||||
|
# - If releasing with the script execute it passing the `-r` flag
|
||||||
|
# plus locale code as argument
|
||||||
|
# $ ./update_translation.sh -r es-ES
|
||||||
|
#
|
||||||
|
# NOTE: WORKFLOW MAINTAINER (CROWDIN)
|
||||||
|
# - Execute the script passing the '-U' flag
|
||||||
|
# $ ./update_translation.sh -U
|
||||||
|
# - Once done, download the translated files, copy them to `translations`
|
||||||
|
# - Upload the updated file to CrowdIn and wait for translators do their thing ;-)
|
||||||
|
# and release all the files to update the changes
|
||||||
|
# $ ./update_translation.sh -R
|
||||||
|
#
|
||||||
|
# --------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
supported_locales=(
|
||||||
|
"en" "af" "ar" "eu" "be" "bg" "ca" "zh-CN" "zh-TW" "hr"
|
||||||
|
"cs" "da" "nl" "fil" "fi" "fr" "gl" "ka" "de" "el"
|
||||||
|
"hu" "id" "it" "ja" "kab" "ko" "lt" "no" "pl" "pt-PT"
|
||||||
|
"pt-BR" "ro" "ru" "sr" "sr-CS" "sk" "sl" "es-ES" "es-AR" "sv-SE"
|
||||||
|
"tr" "uk" "val-ES" "vi"
|
||||||
|
)
|
||||||
|
|
||||||
|
is_locale_supported() {
|
||||||
|
local locale="$1"
|
||||||
|
for supported_locale in "${supported_locales[@]}"; do
|
||||||
|
[ "$supported_locale" == "$locale" ] && return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
update_locale() {
|
||||||
|
local locale="$1"
|
||||||
|
local u=${locale:+_} # Conditional underscore
|
||||||
|
FILES="../*.py ../Resources/ui/*.ui"
|
||||||
|
|
||||||
|
# NOTE: Execute the right commands depending on:
|
||||||
|
# - if it's a locale file or the main, agnostic one
|
||||||
|
[ ! -f "${WB}${u}${locale}.ts" ] && action="Creating" || action="Updating"
|
||||||
|
echo -e "\033[1;34m\n\t<<< ${action} '${WB}${u}${locale}.ts' file >>>\n\033[m"
|
||||||
|
if [ "$u" == "" ]; then
|
||||||
|
eval $LUPDATE "$FILES" -ts "${WB}.ts" # locale-agnostic file
|
||||||
|
else
|
||||||
|
eval $LUPDATE "$FILES" -source-language en_US -target-language "${locale//-/_}" \
|
||||||
|
-ts "${WB}_${locale}.ts"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_crowdin_files() {
|
||||||
|
# Rename files which locales are different on FreeCAD and delete not supported locales
|
||||||
|
crowdin_fixes=(af-ZA ar-SA be-BY bg-BG ca-ES cs-CZ da-DK de-DE el-GR eu-ES fi-FI
|
||||||
|
fil-PH fr-FR gl-ES hr-HR hu-HU it-IT ja-JP ka-GE kab-KAB ko-KR lt-LT nl-NL
|
||||||
|
no-NO pl-PL ro-RO ru-RU sk-SK sl-SI sr-SP tr-TR uk-UA vi-VN)
|
||||||
|
|
||||||
|
crowdin_deletes=(az-AZ bn-BD br-FR bs-BA en en-GB en-US eo-UY es-CO es-VE et-EE fa-IR he-IL
|
||||||
|
hi-IN hy-AM id-ID kaa lv-LV mk-MK ms-MY sat-IN si-LK ta-IN te-IN th-TH ur-PK xav yo-NG)
|
||||||
|
|
||||||
|
for pattern in "${crowdin_fixes[@]}"; do
|
||||||
|
find . -type f -name "*_${pattern}\.*" | while read -r file; do
|
||||||
|
mv -v "$file" "${file//-*./.}"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
for pattern in "${crowdin_deletes[@]}"; do
|
||||||
|
find . -type f -name "*_${pattern}\.*" -delete
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
help() {
|
||||||
|
echo -e "\nDescription:"
|
||||||
|
echo -e "\tCreate, update and release translation files."
|
||||||
|
echo -e "\nUsage:"
|
||||||
|
echo -e "\t./update_translation.sh [-R] [-U] [-r <locale>] [-u <locale>]"
|
||||||
|
echo -e "\nFlags:"
|
||||||
|
echo -e " -R\n\tRelease all translations (qm files)"
|
||||||
|
echo -e " -U\n\tUpdate all translations (ts files)"
|
||||||
|
echo -e " -r <locale>\n\tRelease the specified locale"
|
||||||
|
echo -e " -u <locale>\n\tUpdate strings for the specified locale"
|
||||||
|
echo -e " -N\n\tNormalize CrowdIn filenames"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main function ------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# LUPDATE="C:/Program Files/FreeCAD 1.0/bin/Lib/site-packages/PySide6/lupdate" # from Qt6
|
||||||
|
LUPDATE=/usr/lib/qt6/bin/lupdate # from Qt6
|
||||||
|
# LUPDATE=lupdate # from Qt5
|
||||||
|
# LRELEASE="C:/Program Files/FreeCAD 1.0/bin/Lib/site-packages/PySide6/lrelease" # from Qt6
|
||||||
|
LRELEASE=/usr/lib/qt6/bin/lrelease # from Qt6
|
||||||
|
# LRELEASE=lrelease # from Qt5
|
||||||
|
WB="SearchBar"
|
||||||
|
|
||||||
|
sed -i '3s/-/_/' ${WB}*.ts # Enforce underscore on locales
|
||||||
|
sed -i '3s/\"en\"/\"en_US\"/g' ${WB}*.ts # Use en_US
|
||||||
|
|
||||||
|
if [ $# -eq 1 ]; then
|
||||||
|
if [ "$1" == "-R" ]; then
|
||||||
|
find . -type f -name '*_*.ts' | while IFS= read -r file; do
|
||||||
|
# Release all locales
|
||||||
|
$LRELEASE -nounfinished "$file"
|
||||||
|
echo
|
||||||
|
done
|
||||||
|
elif [ "$1" == "-U" ]; then
|
||||||
|
for locale in "${supported_locales[@]}"; do
|
||||||
|
update_locale "$locale"
|
||||||
|
done
|
||||||
|
elif [ "$1" == "-u" ]; then
|
||||||
|
update_locale # Update main file (agnostic)
|
||||||
|
elif [ "$1" == "-N" ]; then
|
||||||
|
normalize_crowdin_files
|
||||||
|
else
|
||||||
|
help
|
||||||
|
fi
|
||||||
|
elif [ $# -eq 2 ]; then
|
||||||
|
LOCALE="$2"
|
||||||
|
if is_locale_supported "$LOCALE"; then
|
||||||
|
if [ "$1" == "-r" ]; then
|
||||||
|
# Release locale (creation of *.qm file from *.ts file)
|
||||||
|
$LRELEASE -nounfinished "${WB}_${LOCALE}.ts"
|
||||||
|
elif [ "$1" == "-u" ]; then
|
||||||
|
# Update main & locale files
|
||||||
|
update_locale
|
||||||
|
update_locale "$LOCALE"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Verify your language code. Case sensitive."
|
||||||
|
echo "If it's correct, ask a maintainer to add support for your language on FreeCAD."
|
||||||
|
echo -e "\nSupported locales, '\033[1;34mFreeCADGui.supportedLocales()\033[m': \033[1;33m"
|
||||||
|
for locale in $(printf "%s\n" "${supported_locales[@]}" | sort); do
|
||||||
|
echo -n "$locale "
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
help
|
||||||
|
fi
|