Fixed compatibilty issue with QT6 #36

Open
APEbbers wants to merge 60 commits from APEbbers/main into main
32 changed files with 2402 additions and 1011 deletions

142
.gitignore vendored
View File

@ -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
View 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

View 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
View File

@ -0,0 +1,3 @@
{
"githubPullRequests.ignoredPullRequestBranches": ["main"]
}

View File

@ -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"
).refreshToolsResultsProvider(),
getItemGroupsUncached=lambda: [],
)
SearchResults.registerResultProvider(
"document",
getItemGroupsCached=lambda: [], getItemGroupsCached=lambda: [],
getItemGroupsUncached = lambda: __import__('ResultsDocument').documentResultsProvider()) getItemGroupsUncached=lambda: __import__(
SearchResults.registerResultProvider('toolbar', "ResultsDocument"
getItemGroupsCached = lambda: __import__('ResultsToolbar').toolbarResultsProvider(), ).documentResultsProvider(),
getItemGroupsUncached = lambda: []) )
SearchResults.registerResultProvider('param', SearchResults.registerResultProvider(
getItemGroupsCached = lambda: __import__('ResultsPreferences').paramResultsProvider(), "toolbar",
getItemGroupsUncached = lambda: []) 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
),
)

View File

@ -1,28 +1,50 @@
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: if handlerName in SearchResults.actionHandlers:
SearchResults.actionHandlers[handlerName](nfo) SearchResults.actionHandlers[handlerName](nfo)
else: else:
from PySide import QtGui from PySide import QtGui
QtGui.QMessageBox.warning(None, 'Could not execute this action', 'Could not execute this action, it could be from a Mod that has been uninstalled. Try refreshing the list of tools.')
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: if handlerName in SearchResults.toolTipHandlers:
return SearchResults.toolTipHandlers[handlerName](nfo, setParent) return SearchResults.toolTipHandlers[handlerName](nfo, setParent)
else: else:
return 'Could not load tooltip for this tool, it could be from a Mod that has been uninstalled. Try refreshing the list of tools.' 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
@ -35,22 +57,27 @@ def getItemGroups():
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) itemGroups = RefreshTools.refreshToolbars(doLoadAllWorkbenches=False)
else: else:
import Serialize import Serialize_SearchBar
itemGroups = Serialize.deserialize(serializedItemGroups)
itemGroups = Serialize_SearchBar.deserialize(serializedItemGroups)
# Aggregate the tools (cached) and document objects (not cached), and assign an index to each # Aggregate the tools (cached) and document objects (not cached), and assign an index to each
import SearchResults import SearchResults
igs = itemGroups igs = itemGroups
for providerName, provider in SearchResults.resultProvidersUncached.items(): for providerName, provider in SearchResults.resultProvidersUncached.items():
igs = igs + provider() igs = igs + provider()
globalGroups = [] globalGroups = []
def addId(group): def addId(group):
globalGroups.append(group) globalGroups.append(group)
group['id'] = len(globalGroups) - 1 group["id"] = len(globalGroups) - 1
for subitem in group['subitems']: for subitem in group["subitems"]:
addId(subitem) addId(subitem)
for ig in igs: for ig in igs:
addId(ig) addId(ig)

View File

@ -1,10 +1,12 @@
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): def paint(self, painter, option, index):
depth = int(option.widget.model().itemData(index.siblingAtColumn(1))[0]) depth = int(option.widget.model().itemData(index.siblingAtColumn(1))[0])
indent = 16 * depth indent = 16 * depth

View File

@ -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
from PySide import QtGui
import SearchBoxLight
global wax, sea, tbr global wax, sea, tbr
mw = FreeCADGui.getMainWindow() mw = Gui.getMainWindow()
import SearchBox
from PySide.QtWidgets import QToolBar
from PySide.QtGui import QShortcut, QKeySequence
if mw: if mw:
if sea is None: if sea is None:
sea = SearchBoxLight.SearchBoxLight(getItemGroups = lambda: __import__('GetItemGroups').getItemGroups(), wax = SearchBox.SearchBoxFunction(mw)
getToolTip = lambda groupId, setParent: __import__('GetItemGroups').getToolTip(groupId, setParent),
getItemDelegate = lambda: __import__('IndentedItemDelegate').IndentedItemDelegate())
sea.resultSelected.connect(lambda index, groupId: __import__('GetItemGroups').onResultSelected(index, groupId))
if wax is None:
wax = QtGui.QWidgetAction(None)
wax.setWhatsThis('Use this search bar to find tools, document objects, preferences and more')
sea.setWhatsThis('Use this search bar to find tools, document objects, preferences and more')
wax.setDefaultWidget(sea)
##mbr.addWidget(sea)
#mbr.addAction(wax)
if tbr is None: if tbr is None:
tbr = QtGui.QToolBar("SearchBar") #QtGui.QDockWidget() 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. # 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.setObjectName("SearchBar")
tbr.addAction(wax) tbr.addAction(wax)
mw.addToolBar(tbr) mw.addToolBar(tbr)
tbr.show() tbr.show()
return
addToolSearchBox() addToolSearchBox()
import FreeCADGui Gui.getMainWindow().workbenchActivated.connect(addToolSearchBox)
FreeCADGui.getMainWindow().workbenchActivated.connect(addToolSearchBox)

80
Parameters_SearchBar.py Normal file
View 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")

View File

@ -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.
![Screenshot of the search bar, with results in its drop-down menu and extra info about the result in a separate pane](screenshot.png) ![Screenshot of the search bar, with results in its drop-down menu and extra info about the result in a separate pane](Resources/Images/screenshot.png)
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.
![Animation showing how to initially load all workbenches using the first entry in the search bar](animAopt.gif) ![Animation showing how to initially load all workbenches using the first entry in the search bar](Resources/Images/animAopt.gif)
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.
![Animation showing how to navigate the search results with the up and down keys and select code examples from the results](animB2op.gif) ![Animation showing how to navigate the search results with the up and down keys and select code examples from the results](Resources/Images/animB2op.gif)
### 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 [![License: CC0 v1.0.](https://img.shields.io/badge/license-CC0-blue.svg)](https://creativecommons.org/publicdomain/zero/1.0/) ### License [![License: CC0 v1.0.](https://img.shields.io/badge/license-CC0-blue.svg)](https://creativecommons.org/publicdomain/zero/1.0/)
See [LICENSE](LICENSE). See [LICENSE](LICENSE).

View File

@ -1,60 +1,89 @@
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
activeWorkbench = Gui.activeWorkbench().name()
lbl = QLabel(translate("SearchBar", "Loading workbench … (…/…)"))
lbl.setWindowFlags(Qt.WindowType.WindowStaysOnTopHint)
# Get the stylesheet from the main window and use it for this form
lbl.setStyleSheet("background-color: " + StyleMapping_SearchBar.ReturnStyleItem("Background_Color") + ";")
# # Get the main window from FreeCAD
# mw = Gui.getMainWindow()
# # Center the widget
# cp = QGuiApplication.screenAt(mw.pos()).geometry().center()
# lbl.move(cp)
lbl.show() lbl.show()
lst = FreeCADGui.listWorkbenches() lst = Gui.listWorkbenches()
for i, wb in enumerate(lst): for i, wb in enumerate(lst):
msg = 'Loading workbench ' + wb + ' (' + str(i) + '/' + str(len(lst)) + ')' msg = translate("SearchBar", "Loading workbench ") + wb + " (" + str(i + 1) + "/" + str(len(lst)) + ")"
print(msg) print(msg)
lbl.setText(msg) lbl.setText(msg)
geo = lbl.geometry() geo = lbl.geometry()
geo.setSize(lbl.sizeHint()) geo.setSize(lbl.sizeHint())
lbl.setGeometry(geo) lbl.setGeometry(geo)
lbl.repaint() lbl.repaint()
FreeCADGui.updateGui() # Probably slower with this, because it redraws the entire GUI with all tool buttons changed etc. but allows the label to actually be updated, and it looks nice and gives a quick overview of all the workbenches… 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: try:
FreeCADGui.activateWorkbench(wb) Gui.activateWorkbench(wb)
except: except Exception:
pass pass
lbl.hide() lbl.hide()
FreeCADGui.activateWorkbench(activeWorkbench) 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(): for providerName, provider in SearchResults.resultProvidersCached.items():
itemGroups = itemGroups + provider() itemGroups = itemGroups + provider()
return itemGroups return itemGroups
def writeCacheTools(): def writeCacheTools():
import Serialize import Serialize_SearchBar
serializedItemGroups = Serialize.serialize(gatherTools())
serializedItemGroups = Serialize_SearchBar.serialize(gatherTools())
# Todo: use wb and a specific encoding. # Todo: use wb and a specific encoding.
with open(cachePath(), 'w') as cache: with open(cachePath(), "w") as cache:
cache.write(serializedItemGroups) cache.write(serializedItemGroups)
# I prefer to systematically deserialize, instead of taking the original version, # I prefer to systematically deserialize, instead of taking the original version,
# this avoids possible inconsistencies between the original and the cache and # this avoids possible inconsistencies between the original and the cache and
# makes sure cache-related bugs are noticed quickly. # makes sure cache-related bugs are noticed quickly.
import Serialize import Serialize_SearchBar
itemGroups = Serialize.deserialize(serializedItemGroups)
print('SearchBox: Cache has been written.') itemGroups = Serialize_SearchBar.deserialize(serializedItemGroups)
print("SearchBox: Cache has been written.")
return itemGroups 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)
print("SearchBox: Tools were loaded from the cache.")
return itemGroups return itemGroups
@ -68,14 +97,27 @@ def refreshToolbars(doLoadAllWorkbenches = True):
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
mw = Gui.getMainWindow()
reply = msgBox.question(
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() refreshToolbars()
else: else:
print('cancelled') print("cancelled")

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Before

Width:  |  Height:  |  Size: 683 KiB

After

Width:  |  Height:  |  Size: 683 KiB

View File

Before

Width:  |  Height:  |  Size: 404 KiB

After

Width:  |  Height:  |  Size: 404 KiB

View File

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

@ -5,30 +5,46 @@ 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__() 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>' 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 = QtGui.QTextEdit()
description.setReadOnly(True) description.setReadOnly(True)
description.setAlignment(QtCore.Qt.AlignTop) description.setAlignment(QtCore.Qt.AlignTop)
@ -45,9 +61,14 @@ class DocumentObjectToolTipWidget(QtGui.QWidget):
# 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. # 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)) # safeViewerInstance.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed))
self.preview = App._SearchBar3DViewer self.preview = App._SearchBar3DViewer
App._SearchBar3DViewer, App._SearchBar3DViewerB = App._SearchBar3DViewerB, App._SearchBar3DViewer App._SearchBar3DViewer, App._SearchBar3DViewerB = (
App._SearchBar3DViewerB,
App._SearchBar3DViewer,
)
obj = App.getDocument(str(nfo['toolTip']['docName'])).getObject(str(nfo['toolTip']['name'])) obj = App.getDocument(str(nfo["toolTip"]["docName"])).getObject(
str(nfo["toolTip"]["name"])
)
# This is really a bad way to do this… to prevent the setExtraInfo function from # This is really a bad way to do this… to prevent the setExtraInfo function from
# finalizing the object, we remove the parent ourselves. # finalizing the object, we remove the parent ourselves.
@ -77,37 +98,61 @@ class DocumentObjectToolTipWidget(QtGui.QWidget):
# Detach the widget so that it may be reused without getting deleted # Detach the widget so that it may be reused without getting deleted
self.preview.setParent(None) 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): def document(doc):
group = [] group = []
for o in doc.Objects: for o in doc.Objects:
# all_actions.append(lambda: ) # all_actions.append(lambda: )
action = { 'handler': 'documentObject', 'document': o.Document.Name, 'object': o.Name } action = {
"handler": "documentObject",
"document": o.Document.Name,
"object": o.Name,
}
item = { item = {
'icon': o.ViewObject.Icon if o.ViewObject and o.ViewObject.Icon else None, "icon": (
'text': o.Label + ' (' + o.Name + ')', o.ViewObject.Icon if o.ViewObject and o.ViewObject.Icon else None
),
"text": o.Label + " (" + o.Name + ")",
# TODO: preview of the object # TODO: preview of the object
'toolTip': { 'label': o.Label, 'name': o.Name, 'docName': o.Document.Name}, "toolTip": {
'action': action, "label": o.Label,
'subitems': [] "name": o.Name,
"docName": o.Document.Name,
},
"action": action,
"subitems": [],
} }
group.append(item) group.append(item)
action = { 'handler': 'document', 'document': doc.Name } action = {"handler": "document", "document": doc.Name}
itemGroups.append({ itemGroups.append(
'icon': QtGui.QIcon(':/icons/Document.svg'), {
'text': doc.Label + ' (' + doc.Name + ')', "icon": QtGui.QIcon(":/icons/Document.svg"),
"text": doc.Label + " (" + doc.Name + ")",
# TODO: preview of the document # TODO: preview of the document
'toolTip': { 'label': doc.Label, 'name': doc.Name}, "toolTip": {"label": doc.Label, "name": doc.Name},
'action':action, "action": action,
'subitems': group }) "subitems": group,
}
)
if App.ActiveDocument: if App.ActiveDocument:
document(App.ActiveDocument) document(App.ActiveDocument)
for docname, doc in App.listDocuments().items(): for docname, doc in App.listDocuments().items():

View File

@ -2,20 +2,23 @@ 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)
@ -27,64 +30,99 @@ def getParamGroup(grpPath):
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 = [
recur(
False,
path + (":" if atRoot else "/") + child.attrib["Name"],
child.attrib["Name"],
child,
)
for child in tree.getchildren()
if child.tag == "FCParamGroup"
]
return { return {
'icon': QtGui.QIcon(':/icons/Group.svg'), "icon": QtGui.QIcon(":/icons/Group.svg"),
'text': name, "text": name,
'toolTip': '', "toolTip": "",
'action': { 'handler': 'paramGroup', 'path': path, 'name': name }, "action": {"handler": "paramGroup", "path": path, "name": name},
'subitems': params + subgroups "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')]
return [getParamGroups("UserParameter", "User parameter")]
except: except:
print('Could not load the list of all parameters. Please install the LXML python library with:\npython -m pip install --upgrade lxml') print(
"Could not load the list of all parameters. Please install the LXML python library with:\npython -m pip install --upgrade lxml"
)
return [] 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()

View File

@ -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):
return Serialize.iconToHTML(genericToolIcon) + '<p>Load all workbenches to refresh this list of tools. This may take a minute, depending on the number of installed workbenches.</p>'
genericToolIcon = QtGui.QIcon(QtGui.QIcon(os.path.dirname(__file__) + '/Tango-Tools-spanner-hammer.svg')) def refreshToolsToolTip(nfo, setParent):
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>"
)
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": [],
} }
] ]

View File

@ -1,58 +1,113 @@
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(): def runTool():
mw = FreeCADGui.getMainWindow() mw = FreeCADGui.getMainWindow()
for the_toolbar in mw.findChildren(QtGui.QToolBar, act['toolbar']): for the_toolbar in mw.findChildren(QtGui.QToolBar, act["toolbar"]):
for tbt in the_toolbar.findChildren(QtGui.QToolButton): for tbt in the_toolbar.findChildren(QtGui.QToolButton):
if tbt.text() == act['tool']: if tbt.text() == act["tool"]:
action = None action = None
if 'subTool' in act: if "subTool" in act:
men = tbt.menu() men = tbt.menu()
if men: if men:
for mac in men.actions(): for mac in men.actions():
if mac.text() == act['subTool']: if mac.text() == act["subTool"]:
action = mac action = mac
break break
else: else:
action = tbt.defaultAction() action = tbt.defaultAction()
if 'showMenu' in act and act['showMenu']: if "showMenu" in act and act["showMenu"]:
print('Popup submenu of tool ' + toolPath + ' available in workbenches ' + repr(act['workbenches'])) print(
"Popup submenu of tool "
+ toolPath
+ " available in workbenches "
+ repr(act["workbenches"])
)
the_toolbar.show() the_toolbar.show()
tbt.showMenu() tbt.showMenu()
return True return True
elif action is not None: elif action is not None:
print('Run action of tool ' + toolPath + ' available in workbenches ' + repr(act['workbenches'])) print(
"Run action of tool "
+ toolPath
+ " available in workbenches "
+ repr(act["workbenches"])
)
action.trigger() action.trigger()
return True return True
return False return False
if runTool(): if runTool():
return return
else: else:
for workbench in act['workbenches']: for workbench in act["workbenches"]:
print('Activating workbench ' + workbench + ' to access tool ' + toolPath) print("Activating workbench " + workbench + " to access tool " + toolPath)
try:
FreeCADGui.activateWorkbench(workbench) FreeCADGui.activateWorkbench(workbench)
except Exception:
print("SearchBar: Workbench not present! Was it disabled?")
return
if runTool(): if runTool():
return return
print('Tool ' + toolPath + ' not found, was it offered by an extension that is no longer present?') 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()
@ -68,6 +123,7 @@ def getAllToolbars():
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()
@ -79,7 +135,7 @@ def toolbarResultsProvider():
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()
@ -88,18 +144,52 @@ def toolbarResultsProvider():
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",
"workbenches": toolbarIsInWorkbenches,
"toolbar": toolbarName,
"tool": text,
"subTool": mac.text(),
}
subgroup.append(
{
"icon": mac.icon(),
"text": mac.text(),
"toolTip": mac.toolTip(),
"action": action,
"subitems": [],
}
)
# The default action of a menu changes dynamically, instead of triggering the last action, just show the menu. # 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) } action = {
group.append({'icon':icon, 'text':text, 'toolTip': tbt.toolTip(), 'action': action, 'subitems': subgroup}) "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 # TODO: move the 'workbenches' field to the itemgroup
action = { 'handler': 'toolbar', 'workbenches': toolbarIsInWorkbenches, 'toolbar': toolbarName } action = {
itemGroups.append({ "handler": "toolbar",
'icon': QtGui.QIcon(':/icons/Group.svg'), "workbenches": toolbarIsInWorkbenches,
'text': toolbarName, "toolbar": toolbarName,
'toolTip': '', }
'action': action, itemGroups.append(
'subitems': group {
}) "icon": QtGui.QIcon(":/icons/Group.svg"),
"text": toolbarName,
"toolTip": "",
"action": action,
"subitems": group,
}
)
return itemGroups return itemGroups

View File

@ -1,12 +1,21 @@
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) """
enabled = App.ParamGet("User parameter:BaseApp/Preferences/Mod/SearchBar").GetBool(
"PreviewEnabled", False
)
instances = [] instances = []
def __init__(self, parent=None): def __init__(self, parent=None):
super(SafeViewer, self).__init__() super(SafeViewer, self).__init__()
SafeViewer.instances.append(self) SafeViewer.instances.append(self)
@ -18,15 +27,29 @@ class SafeViewer(QtGui.QWidget):
else: else:
import FreeCADGui import FreeCADGui
from PySide import QtCore from PySide import QtCore
self.displaying_warning = True self.displaying_warning = True
self.lbl_warning = QtGui.QTextEdit() self.lbl_warning = QtGui.QTextEdit()
self.lbl_warning.setReadOnly(True) self.lbl_warning.setReadOnly(True)
self.lbl_warning.setAlignment(QtCore.Qt.AlignTop) 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.lbl_warning.setText(
self.btn_enable_for_this_session = QtGui.QPushButton('Enable 3D preview for this session') translate(
self.btn_enable_for_this_session.clicked.connect(self.enable_for_this_session) "SearchBar",
self.btn_enable_for_future_sessions = QtGui.QPushButton('Enable 3D preview for future sessions') "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_future_sessions.clicked.connect(self.enable_for_future_sessions) )
)
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.setLayout(QtGui.QVBoxLayout())
self.layout().addWidget(self.lbl_warning) self.layout().addWidget(self.lbl_warning)
self.layout().addWidget(self.btn_enable_for_this_session) self.layout().addWidget(self.btn_enable_for_this_session)
@ -40,18 +63,21 @@ class SafeViewer(QtGui.QWidget):
def enable_for_future_sessions(self): def enable_for_future_sessions(self):
if not SafeViewer.enabled: if not SafeViewer.enabled:
# Store in prefs # Store in prefs
FreeCAD.ParamGet('User parameter:BaseApp/Preferences/Mod/SearchBar').SetBool('PreviewEnabled', True) App.ParamGet("User parameter:BaseApp/Preferences/Mod/SearchBar").SetBool(
"PreviewEnabled", True
)
# Then enable as usual # Then enable as usual
self.enable_for_this_session() self.enable_for_this_session()
def enable(self): def enable(self):
if not self.instance_enabled: if not self.instance_enabled:
import FreeCADGui import FreeCADGui
# TODO: use a mutex wrapping the entire method, if possible # TODO: use a mutex wrapping the entire method, if possible
SafeViewer.enabled = True SafeViewer.enabled = True
self.instance_enabled = True # Has this specific instance been enabled? self.instance_enabled = True # Has this specific instance been enabled?
if (self.displaying_warning): if self.displaying_warning:
self.layout().removeWidget(self.lbl_warning) self.layout().removeWidget(self.lbl_warning)
self.layout().removeWidget(self.btn_enable_for_this_session) self.layout().removeWidget(self.btn_enable_for_this_session)
self.layout().removeWidget(self.btn_enable_for_future_sessions) self.layout().removeWidget(self.btn_enable_for_future_sessions)
@ -60,7 +86,9 @@ class SafeViewer(QtGui.QWidget):
self.graphicsView = self.viewer.graphicsView() self.graphicsView = self.viewer.graphicsView()
self.oldGraphicsViewParent = self.graphicsView.parent() self.oldGraphicsViewParent = self.graphicsView.parent()
self.oldGraphicsViewParentParent = self.oldGraphicsViewParent.parent() self.oldGraphicsViewParentParent = self.oldGraphicsViewParent.parent()
self.oldGraphicsViewParentParentParent = self.oldGraphicsViewParentParent.parent() self.oldGraphicsViewParentParentParent = (
self.oldGraphicsViewParentParent.parent()
)
# Avoid segfault but still hide the undesired window by moving it to a new hidden MDI area. # Avoid segfault but still hide the undesired window by moving it to a new hidden MDI area.
self.hiddenQMDIArea = QtGui.QMdiArea() self.hiddenQMDIArea = QtGui.QMdiArea()
@ -77,6 +105,7 @@ class SafeViewer(QtGui.QWidget):
slf.finalizer() slf.finalizer()
import weakref import weakref
weakref.finalize(self, fin, self) weakref.finalize(self, fin, self)
self.destroyed.connect(self.finalizer) self.destroyed.connect(self.finalizer)
@ -97,11 +126,13 @@ class SafeViewer(QtGui.QWidget):
def showSceneGraph(self, g): def showSceneGraph(self, g):
import FreeCAD as App import FreeCAD as App
if SafeViewer.enabled: if SafeViewer.enabled:
self.viewer.getViewer().setSceneGraph(g) self.viewer.getViewer().setSceneGraph(g)
self.viewer.setCameraOrientation(App.Rotation(1, 1, 0, 0.2)) self.viewer.setCameraOrientation(App.Rotation(1, 1, 0, 0.2))
self.viewer.fitAll() self.viewer.fitAll()
""" """
# Example use: # Example use:
from PySide import QtGui from PySide import QtGui

View File

@ -1,25 +1,109 @@
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):
def SearchBoxFunction(mw):
import SearchBoxLight
global wax, sea, tbr
if mw:
if sea is None:
sea = SearchBoxLight.SearchBoxLight(
getItemGroups=lambda: __import__("GetItemGroups").getItemGroups(),
getToolTip=lambda groupId, setParent: __import__("GetItemGroups").getToolTip(groupId, setParent),
getItemDelegate=lambda: __import__("IndentedItemDelegate").IndentedItemDelegate(),
)
sea.resultSelected.connect(
lambda index, groupId: __import__("GetItemGroups").onResultSelected(index, groupId)
)
if wax is None:
wax = 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 # The following block of code is present in the lightweight proxy SearchBoxLight
''' """
resultSelected = QtCore.Signal(int, int) resultSelected = QtCore.Signal(int, int)
''' """
@staticmethod @staticmethod
def lazyInit(self): def lazyInit(self):
if self.isInitialized: if self.isInitialized:
@ -29,19 +113,19 @@ class SearchBox(QtGui.QLineEdit):
getItemDelegate = self.getItemDelegate getItemDelegate = self.getItemDelegate
maxVisibleRows = self.maxVisibleRows maxVisibleRows = self.maxVisibleRows
# The following block of code is executed by the lightweight proxy SearchBoxLight # 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
@ -50,54 +134,64 @@ class SearchBox(QtGui.QLineEdit):
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
self.listView.setMouseTracking(True)
# make the QListView non-editable # make the QListView non-editable
self.listView.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) self.listView.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
# Create pane for showing extra info about the currently-selected tool # Create pane for showing extra info about the currently-selected tool
# self.extraInfo = QtGui.QLabel() # self.extraInfo = QtGui.QLabel()
self.extraInfo = QtGui.QWidget() self.extraInfo = QWidget()
self.extraInfo.setWindowFlags(QtGui.Qt.ToolTip) self.extraInfo.setWindowFlags(Qt.WindowType.ToolTip)
self.extraInfo.setWindowFlag(QtGui.Qt.FramelessWindowHint) self.extraInfo.setWindowFlag(Qt.WindowType.FramelessWindowHint)
self.extraInfo.setLayout(QtGui.QVBoxLayout()) self.extraInfo.setLayout(QVBoxLayout())
self.extraInfo.layout().setContentsMargins(0, 0, 0, 0) self.extraInfo.layout().setContentsMargins(0, 0, 0, 0)
self.setExtraInfoIsActive = False self.setExtraInfoIsActive = False
self.pendingExtraInfo = None self.pendingExtraInfo = None
self.currentExtraInfo = None self.currentExtraInfo = None
# Connect signals and slots # Connect signals and slots
self.listView.clicked.connect(lambda x: self.selectResult('select', x)) self.listView.clicked.connect(
self.listView.selectionModel().selectionChanged.connect(self.onSelectionChanged) 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
@ -105,6 +199,27 @@ class SearchBox(QtGui.QLineEdit):
self.isInitialized = True self.isInitialized = True
return self return self
@staticmethod
def proxyMousePressEvent(self, event):
if self.listView.underMouse():
self.selectResult(mode=None, index=self.listView.currentIndex())
else:
event.ignore()
return
@staticmethod
def proxyMouseMoveEvent(self, arg__1):
index = self.listView.indexAt(arg__1.pos())
self.listView.setCurrentIndex(index)
self.setExtraInfo(index)
# Poor attempt to circumvent a glitch where the extra info pane stays visible after pressing Return
if not self.listView.isHidden():
self.showExtraInfo()
if self.listView.isHidden():
self.hideExtraInfo()
return
@staticmethod @staticmethod
def refreshItemGroups(self): def refreshItemGroups(self):
self.itemGroups = self.getItemGroups() self.itemGroups = self.getItemGroups()
@ -112,27 +227,46 @@ class SearchBox(QtGui.QLineEdit):
@staticmethod @staticmethod
def proxyFocusInEvent(self, qFocusEvent): 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: if self.firstShowList:
mdl = QtGui.QStandardItemModel() mdl = QStandardItemModel()
mdl.appendRow([QtGui.QStandardItem(genericToolIcon, 'Please wait, loading results from cache…'), mdl.appendRow(
QtGui.QStandardItem('0'), [
QtGui.QStandardItem('-1')]) QStandardItem(
genericToolIcon,
translate("SearchBar", "Please wait, loading results from cache…"),
),
QStandardItem("0"),
QStandardItem("-1"),
]
)
self.proxyModel.setSourceModel(mdl) self.proxyModel.setSourceModel(mdl)
self.showList() self.showList()
self.firstShowList = False self.firstShowList = False
FreeCADGui.updateGui() Gui.updateGui()
global globalIgnoreFocusOut global globalIgnoreFocusOut
if not globalIgnoreFocusOut: if not globalIgnoreFocusOut:
self.refreshItemGroups() self.refreshItemGroups()
self.showList() self.showList()
super(SearchBoxLight, self).focusInEvent(qFocusEvent) super(SearchBoxLight, self).focusInEvent(qFocusEvent)
return
@staticmethod @staticmethod
def proxyFocusOutEvent(self, qFocusEvent): def proxyFocusOutEvent(self, qFocusEvent):
global globalIgnoreFocusOut global globalIgnoreFocusOut
if not globalIgnoreFocusOut: if not globalIgnoreFocusOut:
self.hideList() self.hideList()
super(SearchBoxLight, self).focusOutEvent(qFocusEvent) # super(SearchBoxLight, self).focusOutEvent(qFocusEvent)
@staticmethod
def proxyLeaveEvent(self, qFocusEvent):
self.clearFocus()
self.hideList()
return
@staticmethod @staticmethod
def movementKey(self, rowUpdate): def movementKey(self, rowUpdate):
@ -147,17 +281,28 @@ class SearchBox(QtGui.QLineEdit):
self.listView.setCurrentIndex(index) self.listView.setCurrentIndex(index)
@staticmethod @staticmethod
def proxyListDown(self): self.movementKey(lambda current, nbRows: (current + 1) % nbRows) def proxyListDown(self):
self.movementKey(lambda current, nbRows: (current + 1) % nbRows)
@staticmethod @staticmethod
def proxyListUp(self): self.movementKey(lambda current, nbRows: (current - 1) % nbRows) def proxyListUp(self):
self.movementKey(lambda current, nbRows: (current - 1) % nbRows)
@staticmethod @staticmethod
def proxyListPageDown(self): self.movementKey(lambda current, nbRows: min(current + max(1, self.maxVisibleRows / 2), nbRows - 1)) def proxyListPageDown(self):
self.movementKey(lambda current, nbRows: min(current + max(1, self.maxVisibleRows / 2), nbRows - 1))
@staticmethod @staticmethod
def proxyListPageUp(self): self.movementKey(lambda current, nbRows: max(current - max(1, self.maxVisibleRows / 2), 0)) def proxyListPageUp(self):
self.movementKey(lambda current, nbRows: max(current - max(1, self.maxVisibleRows / 2), 0))
@staticmethod @staticmethod
def proxyListEnd(self): self.movementKey(lambda current, nbRows: nbRows - 1) def proxyListEnd(self):
self.movementKey(lambda current, nbRows: nbRows - 1)
@staticmethod @staticmethod
def proxyListStart(self): self.movementKey(lambda current, nbRows: 0) def proxyListStart(self):
self.movementKey(lambda current, nbRows: 0)
@staticmethod @staticmethod
def acceptKey(self, mode): def acceptKey(self, mode):
@ -167,9 +312,12 @@ class SearchBox(QtGui.QLineEdit):
self.selectResult(mode, currentIndex) self.selectResult(mode, currentIndex)
@staticmethod @staticmethod
def proxyListAccept(self): self.acceptKey('select') def proxyListAccept(self):
self.acceptKey("select")
@staticmethod @staticmethod
def proxyListAcceptToggle(self): self.acceptKey('toggle') def proxyListAcceptToggle(self):
self.acceptKey("toggle")
@staticmethod @staticmethod
def cancelKey(self): def cancelKey(self):
@ -178,16 +326,17 @@ class SearchBox(QtGui.QLineEdit):
# QKeySequence::Cancel # QKeySequence::Cancel
@staticmethod @staticmethod
def proxyListCancel(self): self.cancelKey() def proxyListCancel(self):
self.cancelKey()
@staticmethod @staticmethod
def proxyKeyPressEvent(self, qKeyEvent): def proxyKeyPressEvent(self, qKeyEvent):
key = qKeyEvent.key() key = qKeyEvent.key()
modifiers = qKeyEvent.modifiers() modifiers = qKeyEvent.modifiers()
self.showList() self.showList()
if key == QtCore.Qt.Key_Home and modifiers & QtCore.Qt.CTRL != 0: if key == Qt.Key.Key_Home and modifiers & Qt.Key.CTRL != 0:
self.listStart() self.listStart()
elif key == QtCore.Qt.Key_End and modifiers & QtCore.Qt.CTRL != 0: elif key == Qt.Key.Key_End and modifiers & Qt.Key.CTRL != 0:
self.listEnd() self.listEnd()
else: else:
super(SearchBoxLight, self).keyPressEvent(qKeyEvent) super(SearchBoxLight, self).keyPressEvent(qKeyEvent)
@ -209,11 +358,11 @@ class SearchBox(QtGui.QLineEdit):
self.extraInfo.hide() self.extraInfo.hide()
@staticmethod @staticmethod
def selectResult(self, mode, index): def selectResult(self, mode: None, index):
groupId = int(index.model().itemData(index.siblingAtColumn(2))[0]) groupId = int(index.model().itemData(index.siblingAtColumn(2))[0])
self.hideList() self.hideList()
# TODO: allow other options, e.g. some items could act as combinators / cumulative filters # TODO: allow other options, e.g. some items could act as combinators / cumulative filters
self.setText('') self.setText("")
self.proxyFilterModel(self.text()) self.proxyFilterModel(self.text())
# TODO: emit index relative to the base model # TODO: emit index relative to the base model
self.resultSelected.emit(index, groupId) self.resultSelected.emit(index, groupId)
@ -223,35 +372,54 @@ class SearchBox(QtGui.QLineEdit):
# TODO: this will cause a race condition if it is accessed while being modified # TODO: this will cause a race condition if it is accessed while being modified
def matches(s): def matches(s):
return userInput.lower() in s.lower() return userInput.lower() in s.lower()
def filterGroup(group): def filterGroup(group):
if matches(group['text']): if matches(group["text"]):
# If a group matches, include the entire subtree (might need to disable this if it causes too much noise) # If a group matches, include the entire subtree (might need to disable this if it causes too much noise)
return group return group
else: else:
subitems = filterGroups(group['subitems']) subitems = filterGroups(group["subitems"])
if len(subitems) > 0 or matches(group['text']): # if len(subitems) == 0:
return { 'id': group['id'], 'text': group['text'], 'icon': group['icon'], 'action': group['action'], 'toolTip':group['toolTip'], 'subitems': subitems } # 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: else:
return None return None
def filterGroups(groups): def filterGroups(groups):
groups = (filterGroup(group) for group in groups) groups = (filterGroup(group) for group in groups)
return [group for group in groups if group is not None] return [group for group in groups if group is not None]
self.mdl = QtGui.QStandardItemModel()
self.mdl = QStandardItemModel()
self.mdl.appendColumn([]) self.mdl.appendColumn([])
def addGroups(filteredGroups, depth=0): def addGroups(filteredGroups, depth=0):
for group in filteredGroups: for group in filteredGroups:
# TODO: this is not very clean, we should memorize the index from the original itemgroups # 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']), self.mdl.appendRow(
QtGui.QStandardItem(str(depth)), [
QtGui.QStandardItem(str(group['id']))]) QStandardItem(group["icon"] or genericToolIcon, group["text"]),
addGroups(group['subitems'], depth+1) QStandardItem(str(depth)),
QStandardItem(str(group["id"])),
]
)
addGroups(group["subitems"], depth + 1)
addGroups(filterGroups(self.itemGroups)) addGroups(filterGroups(self.itemGroups))
self.proxyModel.setSourceModel(self.mdl) self.proxyModel.setSourceModel(self.mdl)
self.currentExtraInfo = None # Unset this so that the ExtraInfo can be updated self.currentExtraInfo = None # Unset this so that the ExtraInfo can be updated
# TODO: try to find the already-highlighted item # 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:
index = self.listView.model().index(0, 0) index = self.listView.model().index(indexSelect, 0)
self.listView.setCurrentIndex(index) self.listView.setCurrentIndex(index)
self.setExtraInfo(index) self.setExtraInfo(index)
else: else:
@ -263,11 +431,12 @@ class SearchBox(QtGui.QLineEdit):
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) pos = getScreenPosition(self)
siz = self.size() siz = self.size()
screen = QtGui.QGuiApplication.screenAt(pos) screen = QApplication.screenAt(pos)
x = pos.x() x = pos.x()
y = pos.y() + siz.height() y = pos.y() + siz.height()
hint_w = self.listView.sizeHint().width() hint_w = self.listView.sizeHint().width()
@ -294,7 +463,7 @@ class SearchBox(QtGui.QLineEdit):
@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()
@ -333,12 +502,13 @@ class SearchBox(QtGui.QLineEdit):
# This is a hack to allow some widgets to set the parent and recompute their size # This is a hack to allow some widgets to set the parent and recompute their size
# during their construction. # during their construction.
parentIsSet = False parentIsSet = False
def setParent(toolTipWidget): def setParent(toolTipWidget):
nonlocal parentIsSet nonlocal parentIsSet
parentIsSet = True parentIsSet = True
w = self.extraInfo.layout().takeAt(0) w = self.extraInfo.layout().takeAt(0)
while w: while w:
if hasattr(w.widget(), 'finalizer'): 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? # 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. # Ensuring that we properly detacth the 3D viewer widget before discarding its parent seems to avoid these crashes.
# print('FINALIZER') # print('FINALIZER')
@ -349,6 +519,7 @@ class SearchBox(QtGui.QLineEdit):
w = self.extraInfo.layout().takeAt(0) w = self.extraInfo.layout().takeAt(0)
self.extraInfo.layout().addWidget(toolTipWidget) self.extraInfo.layout().addWidget(toolTipWidget)
self.setFloatingWidgetsGeometry() self.setFloatingWidgetsGeometry()
toolTipWidget = self.getToolTip(groupId, setParent) toolTipWidget = self.getToolTip(groupId, setParent)
if isinstance(toolTipWidget, str): if isinstance(toolTipWidget, str):
toolTipWidget = easyToolTipWidget(toolTipWidget) toolTipWidget = easyToolTipWidget(toolTipWidget)

View File

@ -1,10 +1,14 @@
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):
def __init__(
self, getItemGroups, getToolTip, getItemDelegate, maxVisibleRows=20, parent=None
):
self.isInitialized = False self.isInitialized = False
# Store arguments # Store arguments
@ -18,32 +22,76 @@ class SearchBoxLight(QtGui.QLineEdit):
# 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 = QtGui.QIcon(":/icons/help-browser.svg")
# ico = QtGui.QIcon(':/icons/WhatsThis.svg') # ico = QtGui.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
def lazyInit(self): def lazyInit(self):
pass pass
def __getattr__(self, name): def __getattr__(self, name):
import types import types
def f(*args, **kwargs): def f(*args, **kwargs):
import SearchBox import SearchBox
SearchBox.SearchBox.lazyInit(self) SearchBox.SearchBox.lazyInit(self)
return getattr(SearchBox.SearchBox, name)(*args, **kwargs) return getattr(SearchBox.SearchBox, name)(*args, **kwargs)
return types.MethodType(f, self) return types.MethodType(f, self)
def focusInEvent(self, *args, **kwargs): return self.proxyFocusInEvent(*args, **kwargs)
def focusOutEvent(self, *args, **kwargs): return self.proxyFocusOutEvent(*args, **kwargs) def MousePressEvent(self, *args, **kwargs):
def keyPressEvent(self, *args, **kwargs): return self.proxyKeyPressEvent(*args, **kwargs) return self.proxyMousePressEvent(*args, **kwargs)
def onSelectionChanged(self, *args, **kwargs): return self.proxyOnSelectionChanged(*args, **kwargs)
def filterModel(self, *args, **kwargs): return self.proxyFilterModel(*args, **kwargs) def MouseMoveEvent(self, *args, **kwargs):
def listDown(self, *args, **kwargs): return self.proxyListDown(*args, **kwargs) return self.proxyMouseMoveEvent(*args, **kwargs)
def listUp(self, *args, **kwargs): return self.proxyListUp(*args, **kwargs)
def listPageDown(self, *args, **kwargs): return self.proxyListPageDown(*args, **kwargs) def LeaveEvent(self, *args, **kwargs):
def listPageUp(self, *args, **kwargs): return self.proxyListPageUp(*args, **kwargs) return self.proxyLeaveEvent(*args, **kwargs)
def listEnd(self, *args, **kwargs): return self.proxyListEnd(*args, **kwargs)
def listStart(self, *args, **kwargs): return self.proxyListStart(*args, **kwargs) def focusInEvent(self, *args, **kwargs):
def listAccept(self, *args, **kwargs): return self.proxyListAccept(*args, **kwargs) return self.proxyFocusInEvent(*args, **kwargs)
def listAcceptToggle(self, *args, **kwargs): return self.proxyListAcceptToggle(*args, **kwargs)
def listCancel(self, *args, **kwargs): return self.proxyListCancel(*args, **kwargs) def focusOutEvent(self, *args, **kwargs):
return self.proxyFocusOutEvent(*args, **kwargs)
def keyPressEvent(self, *args, **kwargs):
return self.proxyKeyPressEvent(*args, **kwargs)
def onSelectionChanged(self, *args, **kwargs):
return self.proxyOnSelectionChanged(*args, **kwargs)
def filterModel(self, *args, **kwargs):
return self.proxyFilterModel(*args, **kwargs)
def listDown(self, *args, **kwargs):
return self.proxyListDown(*args, **kwargs)
def listUp(self, *args, **kwargs):
return self.proxyListUp(*args, **kwargs)
def listPageDown(self, *args, **kwargs):
return self.proxyListPageDown(*args, **kwargs)
def listPageUp(self, *args, **kwargs):
return self.proxyListPageUp(*args, **kwargs)
def listEnd(self, *args, **kwargs):
return self.proxyListEnd(*args, **kwargs)
def listStart(self, *args, **kwargs):
return self.proxyListStart(*args, **kwargs)
def listAccept(self, *args, **kwargs):
return self.proxyListAccept(*args, **kwargs)
def listAcceptToggle(self, *args, **kwargs):
return self.proxyListAcceptToggle(*args, **kwargs)
def listCancel(self, *args, **kwargs):
return self.proxyListCancel(*args, **kwargs)

View File

@ -3,6 +3,7 @@ toolTipHandlers = { }
resultProvidersCached = {} resultProvidersCached = {}
resultProvidersUncached = {} resultProvidersUncached = {}
# name : string # name : string
# getItemGroupsCached: () -> [itemGroup] # getItemGroupsCached: () -> [itemGroup]
# getItemGroupsUncached: () -> [itemGroup] # getItemGroupsUncached: () -> [itemGroup]
@ -10,6 +11,7 @@ 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)

View File

@ -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
View 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
View 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(),
},
}
}

View File

@ -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
View 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>

View 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