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"
getItemGroupsCached = lambda: [], ).refreshToolsResultsProvider(),
getItemGroupsUncached = lambda: __import__('ResultsDocument').documentResultsProvider()) getItemGroupsUncached=lambda: [],
SearchResults.registerResultProvider('toolbar', )
getItemGroupsCached = lambda: __import__('ResultsToolbar').toolbarResultsProvider(), SearchResults.registerResultProvider(
getItemGroupsUncached = lambda: []) "document",
SearchResults.registerResultProvider('param', getItemGroupsCached=lambda: [],
getItemGroupsCached = lambda: __import__('ResultsPreferences').paramResultsProvider(), getItemGroupsUncached=lambda: __import__(
getItemGroupsUncached = lambda: []) "ResultsDocument"
).documentResultsProvider(),
)
SearchResults.registerResultProvider(
"toolbar",
getItemGroupsCached=lambda: __import__("ResultsToolbar").toolbarResultsProvider(),
getItemGroupsUncached=lambda: [],
)
SearchResults.registerResultProvider(
"param",
getItemGroupsCached=lambda: __import__("ResultsPreferences").paramResultsProvider(),
getItemGroupsUncached=lambda: [],
)
SearchResults.registerResultHandler('refreshTools', SearchResults.registerResultHandler(
action = lambda nfo: __import__('ResultsRefreshTools').refreshToolsAction(nfo), "refreshTools",
toolTip = lambda nfo, setParent: __import__('ResultsRefreshTools').refreshToolsToolTip(nfo, setParent)) action=lambda nfo: __import__("ResultsRefreshTools").refreshToolsAction(nfo),
SearchResults.registerResultHandler('toolbar', toolTip=lambda nfo, setParent: __import__(
action = lambda nfo: __import__('ResultsToolbar').toolbarAction(nfo), "ResultsRefreshTools"
toolTip = lambda nfo, setParent: __import__('ResultsToolbar').toolbarToolTip(nfo, setParent)) ).refreshToolsToolTip(nfo, setParent),
SearchResults.registerResultHandler('tool', )
action = lambda nfo : __import__('ResultsToolbar').subToolAction(nfo), SearchResults.registerResultHandler(
toolTip = lambda nfo, setParent: __import__('ResultsToolbar').subToolToolTip(nfo, setParent)) "toolbar",
SearchResults.registerResultHandler('subTool', action=lambda nfo: __import__("ResultsToolbar").toolbarAction(nfo),
action = lambda nfo : __import__('ResultsToolbar').subToolAction(nfo), toolTip=lambda nfo, setParent: __import__("ResultsToolbar").toolbarToolTip(
toolTip = lambda nfo, setParent: __import__('ResultsToolbar').subToolToolTip(nfo, setParent)) nfo, setParent
SearchResults.registerResultHandler('document', ),
action = lambda nfo : __import__('ResultsDocument').documentAction(nfo), )
toolTip = lambda nfo, setParent: __import__('ResultsDocument').documentToolTip(nfo, setParent)) SearchResults.registerResultHandler(
SearchResults.registerResultHandler('documentObject', "tool",
action = lambda nfo : __import__('ResultsDocument').documentObjectAction(nfo), action=lambda nfo: __import__("ResultsToolbar").subToolAction(nfo),
toolTip = lambda nfo, setParent: __import__('ResultsDocument').documentObjectToolTip(nfo, setParent)) toolTip=lambda nfo, setParent: __import__("ResultsToolbar").subToolToolTip(
SearchResults.registerResultHandler('param', nfo, setParent
action = lambda nfo : __import__('ResultsPreferences').paramAction(nfo), ),
toolTip = lambda nfo, setParent: __import__('ResultsPreferences').paramToolTip(nfo, setParent)) )
SearchResults.registerResultHandler('paramGroup', SearchResults.registerResultHandler(
action = lambda nfo : __import__('ResultsPreferences').paramGroupAction(nfo), "subTool",
toolTip = lambda nfo, setParent: __import__('ResultsPreferences').paramGroupToolTip(nfo, setParent)) action=lambda nfo: __import__("ResultsToolbar").subToolAction(nfo),
toolTip=lambda nfo, setParent: __import__("ResultsToolbar").subToolToolTip(
nfo, setParent
),
)
SearchResults.registerResultHandler(
"document",
action=lambda nfo: __import__("ResultsDocument").documentAction(nfo),
toolTip=lambda nfo, setParent: __import__("ResultsDocument").documentToolTip(
nfo, setParent
),
)
SearchResults.registerResultHandler(
"documentObject",
action=lambda nfo: __import__("ResultsDocument").documentObjectAction(nfo),
toolTip=lambda nfo, setParent: __import__("ResultsDocument").documentObjectToolTip(
nfo, setParent
),
)
SearchResults.registerResultHandler(
"param",
action=lambda nfo: __import__("ResultsPreferences").paramAction(nfo),
toolTip=lambda nfo, setParent: __import__("ResultsPreferences").paramToolTip(
nfo, setParent
),
)
SearchResults.registerResultHandler(
"paramGroup",
action=lambda nfo: __import__("ResultsPreferences").paramGroupAction(nfo),
toolTip=lambda nfo, setParent: __import__("ResultsPreferences").paramGroupToolTip(
nfo, setParent
),
)

View File

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

View File

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

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

80
Parameters_SearchBar.py Normal file
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,81 +1,123 @@
import os import os
import FreeCAD as App import FreeCAD as App
import FreeCADGui as Gui
import StyleMapping_SearchBar
# Define the translation
translate = App.Qt.translate
def loadAllWorkbenches(): def loadAllWorkbenches():
from PySide import QtGui import FreeCADGui as Gui
import FreeCADGui from PySide.QtGui import QLabel
activeWorkbench = FreeCADGui.activeWorkbench().name() from PySide.QtCore import Qt, SIGNAL, Signal, QObject, QThread, QSize
lbl = QtGui.QLabel('Loading workbench … (…/…)') from PySide.QtGui import QIcon, QPixmap, QAction, QGuiApplication
lbl.show()
lst = FreeCADGui.listWorkbenches() activeWorkbench = Gui.activeWorkbench().name()
for i, wb in enumerate(lst): lbl = QLabel(translate("SearchBar", "Loading workbench … (…/…)"))
msg = 'Loading workbench ' + wb + ' (' + str(i) + '/' + str(len(lst)) + ')' lbl.setWindowFlags(Qt.WindowType.WindowStaysOnTopHint)
print(msg)
lbl.setText(msg) # Get the stylesheet from the main window and use it for this form
geo = lbl.geometry() lbl.setStyleSheet("background-color: " + StyleMapping_SearchBar.ReturnStyleItem("Background_Color") + ";")
geo.setSize(lbl.sizeHint())
lbl.setGeometry(geo) # # Get the main window from FreeCAD
lbl.repaint() # mw = Gui.getMainWindow()
FreeCADGui.updateGui() # Probably slower with this, because it redraws the entire GUI with all tool buttons changed etc. but allows the label to actually be updated, and it looks nice and gives a quick overview of all the workbenches… # # Center the widget
try: # cp = QGuiApplication.screenAt(mw.pos()).geometry().center()
FreeCADGui.activateWorkbench(wb) # lbl.move(cp)
except:
pass lbl.show()
lbl.hide() lst = Gui.listWorkbenches()
FreeCADGui.activateWorkbench(activeWorkbench) for i, wb in enumerate(lst):
msg = translate("SearchBar", "Loading workbench ") + wb + " (" + str(i + 1) + "/" + str(len(lst)) + ")"
print(msg)
lbl.setText(msg)
geo = lbl.geometry()
geo.setSize(lbl.sizeHint())
lbl.setGeometry(geo)
lbl.repaint()
Gui.updateGui() # Probably slower with this, because it redraws the entire GUI with all tool buttons changed etc. but allows the label to actually be updated, and it looks nice and gives a quick overview of all the workbenches…
try:
Gui.activateWorkbench(wb)
except Exception:
pass
lbl.hide()
Gui.activateWorkbench(activeWorkbench)
return
def cachePath(): def cachePath():
return os.path.join(App.getUserAppDataDir(), 'Cache_SearchBarMod') return os.path.join(App.getUserAppDataDir(), "Cache_SearchBarMod")
def gatherTools(): def gatherTools():
itemGroups = [] itemGroups = []
import SearchResults import SearchResults
for providerName, provider in SearchResults.resultProvidersCached.items():
itemGroups = itemGroups + provider() for providerName, provider in SearchResults.resultProvidersCached.items():
return itemGroups itemGroups = itemGroups + provider()
return itemGroups
def writeCacheTools(): def writeCacheTools():
import Serialize import Serialize_SearchBar
serializedItemGroups = Serialize.serialize(gatherTools())
# Todo: use wb and a specific encoding. serializedItemGroups = Serialize_SearchBar.serialize(gatherTools())
with open(cachePath(), 'w') as cache: # Todo: use wb and a specific encoding.
cache.write(serializedItemGroups) with open(cachePath(), "w") as cache:
# I prefer to systematically deserialize, instead of taking the original version, cache.write(serializedItemGroups)
# this avoids possible inconsistencies between the original and the cache and # I prefer to systematically deserialize, instead of taking the original version,
# makes sure cache-related bugs are noticed quickly. # this avoids possible inconsistencies between the original and the cache and
import Serialize # makes sure cache-related bugs are noticed quickly.
itemGroups = Serialize.deserialize(serializedItemGroups) import Serialize_SearchBar
print('SearchBox: Cache has been written.')
return itemGroups itemGroups = Serialize_SearchBar.deserialize(serializedItemGroups)
print("SearchBox: Cache has been written.")
return itemGroups
def readCacheTools(): def readCacheTools():
# Todo: use rb and a specific encoding. # Todo: use rb and a specific encoding.
with open(cachePath(), 'r') as cache: with open(cachePath(), "r") as cache:
serializedItemGroups = cache.read() serializedItemGroups = cache.read()
import Serialize import Serialize_SearchBar
itemGroups = Serialize.deserialize(serializedItemGroups)
print('SearchBox: Tools were loaded from the cache.') itemGroups = Serialize_SearchBar.deserialize(serializedItemGroups)
return itemGroups print("SearchBox: Tools were loaded from the cache.")
return itemGroups
def refreshToolbars(doLoadAllWorkbenches = True): def refreshToolbars(doLoadAllWorkbenches=True):
if doLoadAllWorkbenches: if doLoadAllWorkbenches:
loadAllWorkbenches() loadAllWorkbenches()
return writeCacheTools() return writeCacheTools()
else: else:
try: try:
return readCacheTools() return readCacheTools()
except: except:
return writeCacheTools() return writeCacheTools()
def refreshToolsAction(): def refreshToolsAction():
from PySide import QtGui from PySide.QtWidgets import QApplication, QMessageBox
print('Refresh list of tools') from PySide.QtCore import Qt
fw = QtGui.QApplication.focusWidget()
if fw is not None: print("Refresh cached results")
fw.clearFocus() msgBox = QMessageBox()
reply = QtGui.QMessageBox.question(None, "Load all workbenches?", "Load all workbenches? This can cause FreeCAD to become unstable, and this \"reload tools\" feature contained a bug that crashed freecad systematically, so please make sure you save your work before. It's a good idea to restart FreeCAD after this operation.", QtGui.QMessageBox.Yes, QtGui.QMessageBox.No) msgBox.setWindowFlags(Qt.WindowType.WindowStaysOnTopHint)
if reply == QtGui.QMessageBox.Yes: # Get the main window from FreeCAD
refreshToolbars() mw = Gui.getMainWindow()
else: reply = msgBox.question(
print('cancelled') mw,
translate("SearchBar", "Load all workbenches?"),
translate(
"SearchBar",
"""Load all workbenches? This can cause FreeCAD to become unstable, and this "reload tools" feature contained a bug that crashed freecad systematically, so please make sure you save your work before. It\'s a good idea to restart FreeCAD after this operation.""",
),
QMessageBox.Yes,
QMessageBox.No,
)
if reply == QMessageBox.Yes:
refreshToolbars()
else:
print("cancelled")

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

View File

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

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): def refreshToolsToolTip(nfo, setParent):
return Serialize.iconToHTML(genericToolIcon) + '<p>Load all workbenches to refresh this list of tools. This may take a minute, depending on the number of installed workbenches.</p>' return (
Serialize_SearchBar.iconToHTML(genericToolIcon)
+ "<p>"
+ translate(
"SearchBar",
"Load all workbenches to refresh the cached results. This may take a minute, depending on the number of installed workbenches.",
)
+ "</p>"
)
genericToolIcon = QtGui.QIcon(QtGui.QIcon(os.path.dirname(__file__) + '/Tango-Tools-spanner-hammer.svg'))
def refreshToolsResultsProvider(): def refreshToolsResultsProvider():
return [ return [
{ {
'icon': genericToolIcon, "icon": genericToolIcon,
'text': 'Refresh list of tools', "text": translate("SearchBar", "Refresh cached results"),
'toolTip': '', "toolTip": "",
'action': {'handler': 'refreshTools'}, "action": {"handler": "refreshTools"},
'subitems': [] "subitems": [],
} }
] ]

View File

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

View File

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

View File

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

View File

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

View File

@ -1,18 +1,20 @@
actionHandlers = { } actionHandlers = {}
toolTipHandlers = { } toolTipHandlers = {}
resultProvidersCached = { } resultProvidersCached = {}
resultProvidersUncached = { } resultProvidersUncached = {}
# name : string # name : string
# getItemGroupsCached: () -> [itemGroup] # getItemGroupsCached: () -> [itemGroup]
# getItemGroupsUncached: () -> [itemGroup] # getItemGroupsUncached: () -> [itemGroup]
def registerResultProvider(name, getItemGroupsCached, getItemGroupsUncached): def registerResultProvider(name, getItemGroupsCached, getItemGroupsUncached):
resultProvidersCached[name] = getItemGroupsCached resultProvidersCached[name] = getItemGroupsCached
resultProvidersUncached[name] = getItemGroupsUncached resultProvidersUncached[name] = getItemGroupsUncached
# name : str # name : str
# action : act -> None # action : act -> None
# toolTip : groupId, setParent -> (str or QWidget) # toolTip : groupId, setParent -> (str or QWidget)
def registerResultHandler(name, action, toolTip): def registerResultHandler(name, action, toolTip):
actionHandlers[name] = action actionHandlers[name] = action
toolTipHandlers[name] = toolTip toolTipHandlers[name] = toolTip

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