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

View File

@ -1,28 +1,50 @@
import FreeCAD as App
import FreeCADGui as Gui
globalGroups = []
itemGroups = None
serializedItemGroups = None
# Define the translation
translate = App.Qt.translate
def onResultSelected(index, groupId):
global globalGroups
nfo = globalGroups[groupId]
handlerName = nfo['action']['handler']
handlerName = nfo["action"]["handler"]
import SearchResults
if handlerName in SearchResults.actionHandlers:
SearchResults.actionHandlers[handlerName](nfo)
else:
from PySide import QtGui
QtGui.QMessageBox.warning(None, 'Could not execute this action', 'Could not execute this action, it could be from a Mod that has been uninstalled. Try refreshing the list of tools.')
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):
global globalGroups
nfo = globalGroups[int(groupId)]
handlerName = nfo['action']['handler']
handlerName = nfo["action"]["handler"]
import SearchResults
if handlerName in SearchResults.toolTipHandlers:
return SearchResults.toolTipHandlers[handlerName](nfo, setParent)
else:
return 'Could not load tooltip for this tool, it could be from a Mod that has been uninstalled. Try refreshing the list of tools.'
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():
global itemGroups, serializedItemGroups, globalGroups
@ -35,22 +57,27 @@ def getItemGroups():
if itemGroups is None:
if serializedItemGroups is None:
import RefreshTools
itemGroups = RefreshTools.refreshToolbars(doLoadAllWorkbenches = False)
itemGroups = RefreshTools.refreshToolbars(doLoadAllWorkbenches=False)
else:
import Serialize
itemGroups = Serialize.deserialize(serializedItemGroups)
import Serialize_SearchBar
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']:
group["id"] = len(globalGroups) - 1
for subitem in group["subitems"]:
addId(subitem)
for ig in igs:
addId(ig)

View File

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

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
wax = None
sea = None
tbr = None
# Define the translation
translate = App.Qt.translate
def QT_TRANSLATE_NOOP(context, text):
return text
def addToolSearchBox():
import FreeCADGui
from PySide import QtGui
import SearchBoxLight
global wax, sea, tbr
mw = FreeCADGui.getMainWindow()
mw = Gui.getMainWindow()
import SearchBox
from PySide.QtWidgets import QToolBar
from PySide.QtGui import QShortcut, QKeySequence
if mw:
if sea is None:
sea = SearchBoxLight.SearchBoxLight(getItemGroups = lambda: __import__('GetItemGroups').getItemGroups(),
getToolTip = lambda groupId, setParent: __import__('GetItemGroups').getToolTip(groupId, setParent),
getItemDelegate = lambda: __import__('IndentedItemDelegate').IndentedItemDelegate())
sea.resultSelected.connect(lambda index, groupId: __import__('GetItemGroups').onResultSelected(index, groupId))
if wax is None:
wax = QtGui.QWidgetAction(None)
wax.setWhatsThis('Use this search bar to find tools, document objects, preferences and more')
sea.setWhatsThis('Use this search bar to find tools, document objects, preferences and more')
wax.setDefaultWidget(sea)
##mbr.addWidget(sea)
#mbr.addAction(wax)
wax = SearchBox.SearchBoxFunction(mw)
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 = 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
addToolSearchBox()
import FreeCADGui
FreeCADGui.getMainWindow().workbenchActivated.connect(addToolSearchBox)
Gui.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.
![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.
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
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.
![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
panel next to the search results provides further documentation about the results, e.g. Python snippets which can be copy-pasted (note:
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.
panel next to the search results provides further documentation about the results, e.g. Python snippets which can be copy-pasted.
![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
@ -64,7 +62,7 @@ Clone the GIT repository or extract the `.zip` downloaded from GitHub to the fol
### 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/)
See [LICENSE](LICENSE).

View File

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

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Before

Width:  |  Height:  |  Size: 683 KiB

After

Width:  |  Height:  |  Size: 683 KiB

View File

Before

Width:  |  Height:  |  Size: 404 KiB

After

Width:  |  Height:  |  Size: 404 KiB

View File

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

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

View File

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

View File

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

View File

@ -1,58 +1,113 @@
import FreeCAD as App
from PySide import QtGui
import FreeCADGui
import Serialize
import Serialize_SearchBar
# Define the translation
translate = App.Qt.translate
def toolbarAction(nfo):
act = nfo['action']
print('show toolbar ' + act['toolbar'] + ' from workbenches ' + repr(act['workbenches']))
act = nfo["action"]
print(
"show toolbar "
+ act["toolbar"]
+ " from workbenches "
+ repr(act["workbenches"])
)
def subToolAction(nfo):
act = nfo['action']
toolPath = act['toolbar'] + '.' + act['tool']
if 'subTool' in act:
toolPath = toolPath + '.' + act['subTool']
act = nfo["action"]
toolPath = act["toolbar"] + "." + act["tool"]
if "subTool" in act:
toolPath = toolPath + "." + act["subTool"]
def runTool():
mw = FreeCADGui.getMainWindow()
for the_toolbar in mw.findChildren(QtGui.QToolBar, act['toolbar']):
for the_toolbar in mw.findChildren(QtGui.QToolBar, act["toolbar"]):
for tbt in the_toolbar.findChildren(QtGui.QToolButton):
if tbt.text() == act['tool']:
if tbt.text() == act["tool"]:
action = None
if 'subTool' in act:
if "subTool" in act:
men = tbt.menu()
if men:
for mac in men.actions():
if mac.text() == act['subTool']:
if mac.text() == act["subTool"]:
action = mac
break
else:
action = tbt.defaultAction()
if 'showMenu' in act and act['showMenu']:
print('Popup submenu of tool ' + toolPath + ' available in workbenches ' + repr(act['workbenches']))
if "showMenu" in act and act["showMenu"]:
print(
"Popup submenu of tool "
+ toolPath
+ " available in workbenches "
+ repr(act["workbenches"])
)
the_toolbar.show()
tbt.showMenu()
return True
elif action is not None:
print('Run action of tool ' + toolPath + ' available in workbenches ' + repr(act['workbenches']))
print(
"Run action of tool "
+ toolPath
+ " available in workbenches "
+ repr(act["workbenches"])
)
action.trigger()
return True
return False
if runTool():
return
else:
for workbench in act['workbenches']:
print('Activating workbench ' + workbench + ' to access tool ' + toolPath)
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?')
print(
"Tool "
+ toolPath
+ " not found, was it offered by an extension that is no longer present?"
)
def toolbarToolTip(nfo, setParent):
workbenches = FreeCADGui.listWorkbenches()
in_workbenches = ['<li>' + (Serialize.iconToHTML(QtGui.QIcon(workbenches[wb].Icon)) if wb in workbenches else '? ') + wb + '</li>' for wb in nfo['action']['workbenches']]
return '<p>Show the ' + nfo['text'] + ' toolbar</p><p>This toolbar appears in the following workbenches: <ul>' + ''.join(in_workbenches) + '</ul></p>'
in_workbenches = [
"<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):
return Serialize.iconToHTML(nfo['icon'], 32) + '<p>' + nfo['toolTip'] + '</p>'
return (
Serialize_SearchBar.iconToHTML(nfo["icon"], 32)
+ "<p>"
+ nfo["toolTip"]
+ "</p>"
)
def getAllToolbars():
all_tbs = dict()
@ -68,6 +123,7 @@ def getAllToolbars():
all_tbs[tb].add(wbname)
return all_tbs
def toolbarResultsProvider():
itemGroups = []
all_tbs = getAllToolbars()
@ -79,7 +135,7 @@ def toolbarResultsProvider():
for tbt in the_toolbar.findChildren(QtGui.QToolButton):
text = tbt.text()
act = tbt.defaultAction()
if text != '':
if text != "":
# TODO: there also is the tooltip
icon = tbt.icon()
men = tbt.menu()
@ -88,18 +144,52 @@ def toolbarResultsProvider():
subgroup = []
for mac in men.actions():
if mac.text():
action = { 'handler': 'subTool', 'workbenches': toolbarIsInWorkbenches, 'toolbar': toolbarName, 'tool': text, 'subTool': mac.text() }
subgroup.append({'icon':mac.icon(), 'text':mac.text(), 'toolTip': mac.toolTip(), 'action':action, 'subitems':[]})
action = {
"handler": "subTool",
"workbenches": toolbarIsInWorkbenches,
"toolbar": toolbarName,
"tool": text,
"subTool": mac.text(),
}
subgroup.append(
{
"icon": mac.icon(),
"text": mac.text(),
"toolTip": mac.toolTip(),
"action": action,
"subitems": [],
}
)
# The default action of a menu changes dynamically, instead of triggering the last action, just show the menu.
action = { 'handler': 'tool', 'workbenches': toolbarIsInWorkbenches, 'toolbar': toolbarName, 'tool': text, 'showMenu': bool(men) }
group.append({'icon':icon, 'text':text, 'toolTip': tbt.toolTip(), 'action': action, 'subitems': subgroup})
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
})
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,13 +1,22 @@
from PySide import QtGui
import FreeCAD
import FreeCAD as App
# Define the translation
translate = App.Qt.translate
class SafeViewer(QtGui.QWidget):
"""FreeCAD uses a modified version of QuarterWidget, so the import pivy.quarter one will cause segfaults.
FreeCAD's FreeCADGui.createViewer() puts the viewer widget inside an MDI window, and detaching it without causing segfaults on exit is tricky.
This class contains some kludges to extract the viewer as a standalone widget and destroy it safely."""
enabled = FreeCAD.ParamGet('User parameter:BaseApp/Preferences/Mod/SearchBar').GetBool('PreviewEnabled', False)
This class contains some kludges to extract the viewer as a standalone widget and destroy it safely.
"""
enabled = App.ParamGet("User parameter:BaseApp/Preferences/Mod/SearchBar").GetBool(
"PreviewEnabled", False
)
instances = []
def __init__(self, parent = None):
def __init__(self, parent=None):
super(SafeViewer, self).__init__()
SafeViewer.instances.append(self)
self.init_parent = parent
@ -18,15 +27,29 @@ class SafeViewer(QtGui.QWidget):
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.lbl_warning.setText(
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)
@ -40,18 +63,21 @@ class SafeViewer(QtGui.QWidget):
def enable_for_future_sessions(self):
if not SafeViewer.enabled:
# Store in prefs
FreeCAD.ParamGet('User parameter:BaseApp/Preferences/Mod/SearchBar').SetBool('PreviewEnabled', True)
App.ParamGet("User parameter:BaseApp/Preferences/Mod/SearchBar").SetBool(
"PreviewEnabled", True
)
# Then enable as usual
self.enable_for_this_session()
def enable(self):
if not self.instance_enabled:
import FreeCADGui
# TODO: use a mutex wrapping the entire method, if possible
SafeViewer.enabled = True
self.instance_enabled = True # Has this specific instance been enabled?
if (self.displaying_warning):
if self.displaying_warning:
self.layout().removeWidget(self.lbl_warning)
self.layout().removeWidget(self.btn_enable_for_this_session)
self.layout().removeWidget(self.btn_enable_for_future_sessions)
@ -60,7 +86,9 @@ class SafeViewer(QtGui.QWidget):
self.graphicsView = self.viewer.graphicsView()
self.oldGraphicsViewParent = self.graphicsView.parent()
self.oldGraphicsViewParentParent = self.oldGraphicsViewParent.parent()
self.oldGraphicsViewParentParentParent = self.oldGraphicsViewParentParent.parent()
self.oldGraphicsViewParentParentParent = (
self.oldGraphicsViewParentParent.parent()
)
# Avoid segfault but still hide the undesired window by moving it to a new hidden MDI area.
self.hiddenQMDIArea = QtGui.QMdiArea()
@ -71,12 +99,13 @@ class SafeViewer(QtGui.QWidget):
self.setLayout(QtGui.QVBoxLayout())
self.layout().addWidget(self.private_widget)
self.layout().setContentsMargins(0,0,0,0)
self.layout().setContentsMargins(0, 0, 0, 0)
def fin(slf):
slf.finalizer()
import weakref
weakref.finalize(self, fin, self)
self.destroyed.connect(self.finalizer)
@ -91,17 +120,19 @@ class SafeViewer(QtGui.QWidget):
self.oldGraphicsViewParent = None
self.graphicsView = None
self.viewer = None
#self.parent = 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.setCameraOrientation(App.Rotation(1, 1, 0, 0.2))
self.viewer.fitAll()
"""
# Example use:
from PySide import QtGui

View File

@ -1,25 +1,109 @@
import FreeCAD as App
import FreeCADGui as Gui
import os
from PySide import QtGui
from PySide import QtCore
import FreeCADGui # just used for FreeCADGui.updateGui()
from PySide.QtCore import (
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
import Parameters_SearchBar as Parameters
genericToolIcon = QIcon(Parameters.genericToolIcon_Pixmap)
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):
foo = QtGui.QTextEdit()
foo = QTextEdit()
foo.setReadOnly(True)
foo.setAlignment(QtCore.Qt.AlignTop)
foo.setAlignment(Qt.AlignmentFlag.AlignTop)
foo.setText(html)
return foo
class SearchBox(QtGui.QLineEdit):
def SearchBoxFunction(mw):
import SearchBoxLight
global wax, sea, tbr
if mw:
if sea is None:
sea = SearchBoxLight.SearchBoxLight(
getItemGroups=lambda: __import__("GetItemGroups").getItemGroups(),
getToolTip=lambda groupId, setParent: __import__("GetItemGroups").getToolTip(groupId, setParent),
getItemDelegate=lambda: __import__("IndentedItemDelegate").IndentedItemDelegate(),
)
sea.resultSelected.connect(
lambda index, groupId: __import__("GetItemGroups").onResultSelected(index, groupId)
)
if wax is None:
wax = QWidgetAction(None)
wax.setWhatsThis(
translate(
"SearchBar",
"Use this search bar to find tools, document objects, preferences and more",
)
)
sea.setWhatsThis(
translate(
"SearchBar",
"Use this search bar to find tools, document objects, preferences and more",
)
)
wax.setDefaultWidget(sea)
return wax
class SearchBox(QLineEdit):
# The following block of code is present in the lightweight proxy SearchBoxLight
'''
"""
resultSelected = QtCore.Signal(int, int)
'''
"""
@staticmethod
def lazyInit(self):
if self.isInitialized:
@ -29,82 +113,113 @@ class SearchBox(QtGui.QLineEdit):
getItemDelegate = self.getItemDelegate
maxVisibleRows = self.maxVisibleRows
# The following block of code is executed by the lightweight proxy SearchBoxLight
'''
"""
# Call parent constructor
super(SearchBoxLight, self).__init__(parent)
# Connect signals and slots
self.textChanged.connect(self.filterModel)
# Thanks to https://saurabhg.com/programming/search-box-using-qlineedit/ for indicating a few useful options
ico = QtGui.QIcon(':/icons/help-browser.svg')
#ico = QtGui.QIcon(':/icons/WhatsThis.svg')
ico = QIcon(':/icons/help-browser.svg')
#ico = QIcon(':/icons/WhatsThis.svg')
self.addAction(ico, QtGui.QLineEdit.LeadingPosition)
self.setClearButtonEnabled(True)
self.setPlaceholderText('Search tools, prefs & tree')
self.setFixedWidth(200) # needed to avoid a change of width when the clear button appears/disappears
'''
"""
# Save arguments
#self.model = model
# self.model = model
self.getItemGroups = getItemGroups
self.getToolTip = getToolTip
self.itemGroups = None # Will be initialized by calling getItemGroups() the first time the search box gains focus, through focusInEvent and refreshItemGroups
self.maxVisibleRows = maxVisibleRows # TODO: use this to compute the correct height
# Create proxy model
self.proxyModel = QtCore.QIdentityProxyModel()
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.
self.mdl = QtGui.QStandardItemModel()
#self.proxyModel.setModel(self.model)
self.mdl = QStandardItemModel()
# self.proxyModel.setModel(self.model)
# Create list view
self.listView = QtGui.QListView(self)
self.listView.setWindowFlags(QtGui.Qt.ToolTip)
self.listView.setWindowFlag(QtGui.Qt.FramelessWindowHint)
self.listView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
self.listView = QListView(self)
self.listView.setWindowFlags(Qt.WindowType.ToolTip)
self.listView.setWindowFlag(Qt.WindowType.FramelessWindowHint)
self.listView.setSelectionMode(self.listView.SelectionMode.SingleSelection)
self.listView.setModel(self.proxyModel)
self.listView.setItemDelegate(getItemDelegate()) # https://stackoverflow.com/a/65930408/324969
self.listView.setMouseTracking(True)
# make the QListView non-editable
self.listView.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
self.listView.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
# Create pane for showing extra info about the currently-selected tool
#self.extraInfo = QtGui.QLabel()
self.extraInfo = QtGui.QWidget()
self.extraInfo.setWindowFlags(QtGui.Qt.ToolTip)
self.extraInfo.setWindowFlag(QtGui.Qt.FramelessWindowHint)
self.extraInfo.setLayout(QtGui.QVBoxLayout())
self.extraInfo.layout().setContentsMargins(0,0,0,0)
# self.extraInfo = QtGui.QLabel()
self.extraInfo = QWidget()
self.extraInfo.setWindowFlags(Qt.WindowType.ToolTip)
self.extraInfo.setWindowFlag(Qt.WindowType.FramelessWindowHint)
self.extraInfo.setLayout(QVBoxLayout())
self.extraInfo.layout().setContentsMargins(0, 0, 0, 0)
self.setExtraInfoIsActive = False
self.pendingExtraInfo = None
self.currentExtraInfo = None
# Connect signals and slots
self.listView.clicked.connect(lambda x: self.selectResult('select', x))
self.listView.selectionModel().selectionChanged.connect(self.onSelectionChanged)
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...
wdgctx = QtCore.Qt.ShortcutContext.WidgetShortcut
wdgctx = Qt.ShortcutContext.WidgetShortcut
QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Down), self, context = wdgctx).activated.connect(self.listDown)
QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Up), self, context = wdgctx).activated.connect(self.listUp)
QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_PageDown), self, context = wdgctx).activated.connect(self.listPageDown)
QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_PageUp), self, context = wdgctx).activated.connect(self.listPageUp)
QShortcut(QKeySequence(Qt.Key.Key_Down), self, context=wdgctx).activated.connect(self.listDown)
QShortcut(QKeySequence(Qt.Key.Key_Up), self, context=wdgctx).activated.connect(self.listUp)
QShortcut(QKeySequence(Qt.Key.Key_PageDown), self, context=wdgctx).activated.connect(self.listPageDown)
QShortcut(QKeySequence(Qt.Key.Key_PageUp), self, context=wdgctx).activated.connect(self.listPageUp)
# Home and End do not work, for some reason.
#QtGui.QShortcut(QtGui.QKeySequence.MoveToEndOfDocument, self, context = wdgctx).activated.connect(self.listEnd)
#QtGui.QShortcut(QtGui.QKeySequence.MoveToStartOfDocument, self, context = wdgctx).activated.connect(self.listStart)
#QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_End), self, context = wdgctx).activated.connect(self.listEnd)
#QtGui.QShortcut(QtGui.QKeySequence('Home'), self, context = wdgctx).activated.connect(self.listStart)
# QShortcut(QKeySequence.MoveToEndOfDocument, self, context = wdgctx).activated.connect(self.listEnd)
# QShortcut(QKeySequence.MoveToStartOfDocument, self, context = wdgctx).activated.connect(self.listStart)
# QShortcut(QKeySequence(Qt.Key.Key_End), self, context = wdgctx).activated.connect(self.listEnd)
# 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)
QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Return), self, context = wdgctx).activated.connect(self.listAccept)
QtGui.QShortcut(QtGui.QKeySequence('Ctrl+Return'), self, context = wdgctx).activated.connect(self.listAcceptToggle)
QtGui.QShortcut(QtGui.QKeySequence('Ctrl+Enter'), self, context = wdgctx).activated.connect(self.listAcceptToggle)
QtGui.QShortcut(QtGui.QKeySequence('Ctrl+Space'), self, context = wdgctx).activated.connect(self.listAcceptToggle)
QShortcut(QKeySequence(Qt.Key.Key_Enter), self, context=wdgctx).activated.connect(self.listAccept)
QShortcut(QKeySequence(Qt.Key.Key_Return), self, context=wdgctx).activated.connect(self.listAccept)
QShortcut(QKeySequence("Ctrl+Return"), self, context=wdgctx).activated.connect(self.listAcceptToggle)
QShortcut(QKeySequence("Ctrl+Enter"), 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)
#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.isInitialized = True
return self
@staticmethod
def proxyMousePressEvent(self, event):
if self.listView.underMouse():
self.selectResult(mode=None, index=self.listView.currentIndex())
else:
event.ignore()
return
@staticmethod
def proxyMouseMoveEvent(self, arg__1):
index = self.listView.indexAt(arg__1.pos())
self.listView.setCurrentIndex(index)
self.setExtraInfo(index)
# Poor attempt to circumvent a glitch where the extra info pane stays visible after pressing Return
if not self.listView.isHidden():
self.showExtraInfo()
if self.listView.isHidden():
self.hideExtraInfo()
return
@staticmethod
def refreshItemGroups(self):
self.itemGroups = self.getItemGroups()
@ -112,27 +227,46 @@ class SearchBox(QtGui.QLineEdit):
@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 = QtGui.QStandardItemModel()
mdl.appendRow([QtGui.QStandardItem(genericToolIcon, 'Please wait, loading results from cache…'),
QtGui.QStandardItem('0'),
QtGui.QStandardItem('-1')])
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
FreeCADGui.updateGui()
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)
# super(SearchBoxLight, self).focusOutEvent(qFocusEvent)
@staticmethod
def proxyLeaveEvent(self, qFocusEvent):
self.clearFocus()
self.hideList()
return
@staticmethod
def movementKey(self, rowUpdate):
@ -147,17 +281,28 @@ class SearchBox(QtGui.QLineEdit):
self.listView.setCurrentIndex(index)
@staticmethod
def proxyListDown(self): self.movementKey(lambda current, nbRows: (current + 1) % nbRows)
def proxyListDown(self):
self.movementKey(lambda current, nbRows: (current + 1) % nbRows)
@staticmethod
def proxyListUp(self): self.movementKey(lambda current, nbRows: (current - 1) % nbRows)
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))
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))
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)
def proxyListEnd(self):
self.movementKey(lambda current, nbRows: nbRows - 1)
@staticmethod
def proxyListStart(self): self.movementKey(lambda current, nbRows: 0)
def proxyListStart(self):
self.movementKey(lambda current, nbRows: 0)
@staticmethod
def acceptKey(self, mode):
@ -167,9 +312,12 @@ class SearchBox(QtGui.QLineEdit):
self.selectResult(mode, currentIndex)
@staticmethod
def proxyListAccept(self): self.acceptKey('select')
def proxyListAccept(self):
self.acceptKey("select")
@staticmethod
def proxyListAcceptToggle(self): self.acceptKey('toggle')
def proxyListAcceptToggle(self):
self.acceptKey("toggle")
@staticmethod
def cancelKey(self):
@ -178,16 +326,17 @@ class SearchBox(QtGui.QLineEdit):
# QKeySequence::Cancel
@staticmethod
def proxyListCancel(self): self.cancelKey()
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:
if key == Qt.Key.Key_Home and modifiers & Qt.Key.CTRL != 0:
self.listStart()
elif key == QtCore.Qt.Key_End and modifiers & QtCore.Qt.CTRL != 0:
elif key == Qt.Key.Key_End and modifiers & Qt.Key.CTRL != 0:
self.listEnd()
else:
super(SearchBoxLight, self).keyPressEvent(qKeyEvent)
@ -209,11 +358,11 @@ class SearchBox(QtGui.QLineEdit):
self.extraInfo.hide()
@staticmethod
def selectResult(self, mode, index):
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.setText("")
self.proxyFilterModel(self.text())
# TODO: emit index relative to the base model
self.resultSelected.emit(index, groupId)
@ -223,51 +372,71 @@ class SearchBox(QtGui.QLineEdit):
# 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 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 }
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 = QtGui.QStandardItemModel()
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([QtGui.QStandardItem(group['icon'] or genericToolIcon, group['text']),
QtGui.QStandardItem(str(depth)),
QtGui.QStandardItem(str(group['id']))])
addGroups(group['subitems'], depth+1)
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()
if nbRows > 0:
index = self.listView.model().index(0, 0)
index = self.listView.model().index(indexSelect, 0)
self.listView.setCurrentIndex(index)
self.setExtraInfo(index)
else:
self.clearExtraInfo()
#self.showList()
# self.showList()
@staticmethod
def setFloatingWidgetsGeometry(self):
def getScreenPosition(widget):
geo = widget.geometry()
parent = widget.parent()
parentPos = getScreenPosition(parent) if parent is not None else QtCore.QPoint(0,0)
return QtCore.QPoint(geo.x() + parentPos.x(), geo.y() + parentPos.y())
parentPos = getScreenPosition(parent) if parent is not None else QPoint(0, 0)
return QPoint(geo.x() + parentPos.x(), geo.y() + parentPos.y())
pos = getScreenPosition(self)
siz = self.size()
screen = QtGui.QGuiApplication.screenAt(pos)
screen = QApplication.screenAt(pos)
x = pos.x()
y = pos.y() + siz.height()
hint_w = self.listView.sizeHint().width()
@ -294,7 +463,7 @@ class SearchBox(QtGui.QLineEdit):
@staticmethod
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
# index in deselected.indexes()
selected = selected.indexes()
@ -318,10 +487,10 @@ class SearchBox(QtGui.QLineEdit):
# TODO: use an atomic swap or mutex if possible
if self.setExtraInfoIsActive:
self.pendingExtraInfo = index
#print("boom")
# print("boom")
else:
self.setExtraInfoIsActive = True
#print("lock")
# 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
@ -333,15 +502,16 @@ class SearchBox(QtGui.QLineEdit):
# 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'):
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')
# 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.
@ -349,6 +519,7 @@ class SearchBox(QtGui.QLineEdit):
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)
@ -359,7 +530,7 @@ class SearchBox(QtGui.QLineEdit):
self.pendingExtraInfo = None
else:
break
#print("unlock")
# print("unlock")
self.setExtraInfoIsActive = False
@staticmethod

View File

@ -1,10 +1,14 @@
from PySide import QtGui
from PySide import QtCore
# This is a "light" version of the SearchBox implementation, which loads the actual implementation on first click
class SearchBoxLight(QtGui.QLineEdit):
resultSelected = QtCore.Signal(int, int)
def __init__(self, getItemGroups, getToolTip, getItemDelegate, maxVisibleRows = 20, parent = None):
def __init__(
self, getItemGroups, getToolTip, getItemDelegate, maxVisibleRows=20, parent=None
):
self.isInitialized = False
# Store arguments
@ -18,32 +22,76 @@ class SearchBoxLight(QtGui.QLineEdit):
# Connect signals and slots
self.textChanged.connect(self.filterModel)
# Thanks to https://saurabhg.com/programming/search-box-using-qlineedit/ for indicating a few useful options
ico = QtGui.QIcon(':/icons/help-browser.svg')
#ico = QtGui.QIcon(':/icons/WhatsThis.svg')
ico = QtGui.QIcon(":/icons/help-browser.svg")
# ico = QtGui.QIcon(':/icons/WhatsThis.svg')
self.addAction(ico, QtGui.QLineEdit.LeadingPosition)
self.setClearButtonEnabled(True)
self.setPlaceholderText('Search tools, prefs & tree')
self.setFixedWidth(200) # needed to avoid a change of width when the clear button appears/disappears
self.setPlaceholderText("Search tools, prefs & tree")
self.setFixedWidth(
200
) # needed to avoid a change of width when the clear button appears/disappears
def lazyInit(self):
pass
def __getattr__(self, name):
import types
def f(*args, **kwargs):
import SearchBox
SearchBox.SearchBox.lazyInit(self)
return getattr(SearchBox.SearchBox, name)(*args, **kwargs)
return types.MethodType(f, self)
def focusInEvent(self, *args, **kwargs): return self.proxyFocusInEvent(*args, **kwargs)
def focusOutEvent(self, *args, **kwargs): return self.proxyFocusOutEvent(*args, **kwargs)
def keyPressEvent(self, *args, **kwargs): return self.proxyKeyPressEvent(*args, **kwargs)
def onSelectionChanged(self, *args, **kwargs): return self.proxyOnSelectionChanged(*args, **kwargs)
def filterModel(self, *args, **kwargs): return self.proxyFilterModel(*args, **kwargs)
def listDown(self, *args, **kwargs): return self.proxyListDown(*args, **kwargs)
def listUp(self, *args, **kwargs): return self.proxyListUp(*args, **kwargs)
def listPageDown(self, *args, **kwargs): return self.proxyListPageDown(*args, **kwargs)
def listPageUp(self, *args, **kwargs): return self.proxyListPageUp(*args, **kwargs)
def listEnd(self, *args, **kwargs): return self.proxyListEnd(*args, **kwargs)
def listStart(self, *args, **kwargs): return self.proxyListStart(*args, **kwargs)
def listAccept(self, *args, **kwargs): return self.proxyListAccept(*args, **kwargs)
def listAcceptToggle(self, *args, **kwargs): return self.proxyListAcceptToggle(*args, **kwargs)
def listCancel(self, *args, **kwargs): return self.proxyListCancel(*args, **kwargs)
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,7 +1,8 @@
actionHandlers = { }
toolTipHandlers = { }
resultProvidersCached = { }
resultProvidersUncached = { }
actionHandlers = {}
toolTipHandlers = {}
resultProvidersCached = {}
resultProvidersUncached = {}
# name : string
# getItemGroupsCached: () -> [itemGroup]
@ -10,6 +11,7 @@ def registerResultProvider(name, getItemGroupsCached, getItemGroupsUncached):
resultProvidersCached[name] = getItemGroupsCached
resultProvidersUncached[name] = getItemGroupsUncached
# name : str
# action : act -> None
# toolTip : groupId, setParent -> (str or QWidget)

View File

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

156
Serialize_SearchBar.py Normal file
View File

@ -0,0 +1,156 @@
from PySide import QtCore
from PySide import QtGui
import json
def iconToBase64(
icon: QtGui.QIcon,
sz=QtCore.QSize(64, 64),
mode=QtGui.QIcon.Mode.Normal,
state=QtGui.QIcon.State.On,
):
"""
Converts a QIcon to a Base64-encoded string representation of its pixmap.
Args:
icon (QIcon): The icon to encode.
sz (QSize): The size of the pixmap to generate.
mode (QIcon.Mode): The mode of the pixmap (e.g., Normal, Disabled).
state (QIcon.State): The state of the pixmap (e.g., On, Off).
Returns:
str: The Base64-encoded string of the icon's pixmap.
"""
buf = QtCore.QBuffer()
buf.open(QtCore.QIODevice.OpenModeFlag.WriteOnly)
# Save the pixmap of the icon to the buffer in PNG format
pixmap: QtGui.QPixmap = icon.pixmap(sz, mode, state)
try:
pixmap.save(buf, "PNG")
except Exception as e:
# raise ValueError("Failed to save icon to buffer. Ensure the icon is valid.")
print(e)
# Use standard Base64 encoding
base64_data = buf.data().toBase64().data().decode("utf-8")
buf.close()
return base64_data
def iconToHTML(icon, sz=12, mode=QtGui.QIcon.Mode.Normal, state=QtGui.QIcon.State.On):
return (
'<img width="'
+ str(sz)
+ '" height="'
+ str(sz)
+ '" src="data:image/png;base64,'
+ iconToBase64(icon, QtCore.QSize(sz, sz), mode, state)
+ '" />'
)
def serializeIcon(icon):
iconPixmaps = {}
for sz in icon.availableSizes():
strW = str(sz.width())
strH = str(sz.height())
iconPixmaps[strW] = {}
iconPixmaps[strW][strH] = {}
for strMode, mode in {
"normal": QtGui.QIcon.Mode.Normal,
"disabled": QtGui.QIcon.Mode.Disabled,
"active": QtGui.QIcon.Mode.Active,
"selected": QtGui.QIcon.Mode.Selected,
}.items():
iconPixmaps[strW][strH][strMode] = {}
for strState, state in {
"off": QtGui.QIcon.State.Off,
"on": QtGui.QIcon.State.On,
}.items():
iconPixmaps[strW][strH][strMode][strState] = iconToBase64(
icon, sz, mode, state
)
return iconPixmaps
# workbenches is a list(str), toolbar is a str, text is a str, icon is a QtGui.QIcon
def serializeTool(tool):
return {
"workbenches": tool["workbenches"],
"toolbar": tool["toolbar"],
"text": tool["text"],
"toolTip": tool["toolTip"],
"icon": serializeIcon(tool["icon"]),
}
def deserializeIcon(iconPixmaps):
ico = QtGui.QIcon()
for strW, wPixmaps in iconPixmaps.items():
for strH, hPixmaps in wPixmaps.items():
for strMode, modePixmaps in hPixmaps.items():
mode = {
"normal": QtGui.QIcon.Mode.Normal,
"disabled": QtGui.QIcon.Mode.Disabled,
"active": QtGui.QIcon.Mode.Active,
"selected": QtGui.QIcon.Mode.Selected,
}[strMode]
for strState, statePixmap in modePixmaps.items():
state = {"off": QtGui.QIcon.State.Off, "on": QtGui.QIcon.State.On}[
strState
]
pxm = QtGui.QPixmap()
pxm.loadFromData(
QtCore.QByteArray.fromBase64(
bytearray(statePixmap.encode("utf-8"))
)
)
ico.addPixmap(pxm, mode, state)
return ico
def deserializeTool(tool):
return {
"workbenches": tool["workbenches"],
"toolbar": tool["toolbar"],
"text": tool["text"],
"toolTip": tool["toolTip"],
"icon": deserializeIcon(tool["icon"]),
}
def serializeItemGroup(itemGroup):
return {
"icon": serializeIcon(itemGroup["icon"]),
"text": itemGroup["text"],
"toolTip": itemGroup["toolTip"],
"action": itemGroup["action"],
"subitems": serializeItemGroups(itemGroup["subitems"]),
}
def serializeItemGroups(itemGroups):
return [serializeItemGroup(itemGroup) for itemGroup in itemGroups]
def serialize(itemGroups):
return json.dumps(serializeItemGroups(itemGroups))
def deserializeItemGroup(itemGroup):
return {
"icon": deserializeIcon(itemGroup["icon"]),
"text": itemGroup["text"],
"toolTip": itemGroup["toolTip"],
"action": itemGroup["action"],
"subitems": deserializeItemGroups(itemGroup["subitems"]),
}
def deserializeItemGroups(serializedItemGroups):
return [deserializeItemGroup(itemGroup) for itemGroup in serializedItemGroups]
def deserialize(serializedItemGroups):
return deserializeItemGroups(json.loads(serializedItemGroups))

239
StyleMapping_SearchBar.py Normal file
View File

@ -0,0 +1,239 @@
# *************************************************************************
# * *
# * Copyright (c) 2019-2024 Paul Ebbers *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 3 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# *************************************************************************
import FreeCAD as App
import FreeCADGui as Gui
import os
from PySide.QtGui import QIcon, QPixmap, QAction
from PySide.QtWidgets import (
QListWidgetItem,
QTableWidgetItem,
QListWidget,
QTableWidget,
QToolBar,
QToolButton,
QComboBox,
QPushButton,
QMenu,
QWidget,
QMainWindow,
)
from PySide.QtCore import Qt, SIGNAL, Signal, QObject, QThread
import sys
import json
from datetime import datetime
import shutil
import Standard_Functions_RIbbon as StandardFunctions
import Parameters_Ribbon
import webbrowser
import time
# Get the resources
pathIcons = Parameters_Ribbon.ICON_LOCATION
pathStylSheets = Parameters_Ribbon.STYLESHEET_LOCATION
pathUI = Parameters_Ribbon.UI_LOCATION
pathBackup = Parameters_Ribbon.BACKUP_LOCATION
sys.path.append(pathIcons)
sys.path.append(pathStylSheets)
sys.path.append(pathUI)
sys.path.append(pathBackup)
def ReturnStyleItem(ControlName, ShowCustomIcon=False, IgnoreOverlay=False):
"""
Enter one of the names below:
ControlName (string):
"Background_Color" returns string,
"Border_Color" returns string,
"FontColor" returns string,
"FontColor" returns string,
"""
# define a result holder and a dict for the StyleMapping file
result = "none"
# Get the current stylesheet for FreeCAD
FreeCAD_preferences = App.ParamGet("User parameter:BaseApp/Preferences/MainWindow")
currentStyleSheet = FreeCAD_preferences.GetString("StyleSheet")
IsInList = False
for key, value in StyleMapping_default["Stylesheets"].items():
if key == currentStyleSheet:
IsInList = True
break
if IsInList is False:
currentStyleSheet = "none"
try:
result = StyleMapping_default["Stylesheets"][currentStyleSheet][ControlName]
if result == "" or result is None:
result = StyleMapping_default["Stylesheets"][""][ControlName]
return result
except Exception as e:
print(e)
return None
def ReturnColor(ColorType="Background_Color"):
mw: QMainWindow = Gui.getMainWindow()
palette = mw.style().standardPalette()
# Get the color
Color = palette.base().color().toTuple() # RGBA tupple
if ColorType == "Border_Color":
Color = palette.buttonText().color().toTuple()
if ColorType == "Background_Color_Hover":
Color = palette.highlight().color().toTuple()
HexColor = StandardFunctions.ColorConvertor(Color, Color[3] / 255, True, False)
return HexColor
def ReturnFontColor():
fontColor = "#000000"
IsDarkTheme = DarkMode()
if IsDarkTheme is True:
fontColor = "#ffffff"
return fontColor
def DarkMode():
import xml.etree.ElementTree as ET
import os
# Define the standard result
IsDarkTheme = False
# Get the current stylesheet for FreeCAD
FreeCAD_preferences = App.ParamGet("User parameter:BaseApp/Preferences/MainWindow")
currentStyleSheet = FreeCAD_preferences.GetString("StyleSheet")
path = os.path.dirname(__file__)
# Get the folder with add-ons
for i in range(2):
# Starting point
path = os.path.dirname(path)
# Go through the sub-folders
for root, dirs, files in os.walk(path):
for name in dirs:
# if the current stylesheet matches a sub directory, try to geth the pacakgexml
if currentStyleSheet.replace(".qss", "").lower() in name.lower():
try:
packageXML = os.path.join(path, name, "package.xml")
# Get the tree and root of the xml file
tree = ET.parse(packageXML)
treeRoot = tree.getroot()
# Get all the tag elements
elements = []
namespaces = {"i": "https://wiki.freecad.org/Package_Metadata"}
elements = treeRoot.findall(".//i:content/i:preferencepack/i:tag", namespaces)
# go throug all tags. If 'dark' in the element text, this is a dark theme
for element in elements:
if "dark" in element.text.lower():
IsDarkTheme = True
break
except Exception:
continue
return IsDarkTheme
StyleMapping_default = {
"Stylesheets": {
"": {
"Background_Color": "#f0f0f0",
"Background_Color_Hover": "#ced4da",
"Border_Color": "#646464",
"FontColor": ReturnFontColor(),
},
"none": {
"Background_Color": "none",
"Background_Color_Hover": "#48a0f8",
"Border_Color": ReturnColor("Border_Color"),
"FontColor": ReturnFontColor(),
},
"FreeCAD Dark.qss": {
"Background_Color": "#333333",
"Background_Color_Hover": "#48a0f8",
"Border_Color": "#ffffff",
"FontColor": "#ffffff",
},
"FreeCAD Light.qss": {
"Background_Color": "#f0f0f0",
"Background_Color_Hover": "#48a0f8",
"Border_Color": "#646464",
"FontColor": "#000000",
},
"OpenLight.qss": {
"Background_Color": "#dee2e6",
"Background_Color_Hover": "#a5d8ff",
"Border_Color": "#1c7ed6",
"FontColor": "#000000",
},
"OpenDark.qss": {
"Background_Color": "#212529",
"Background_Color_Hover": "#1f364d",
"Border_Color": "#264b69",
"FontColor": "#ffffff",
},
"Behave-dark.qss": {
"Background_Color": "#232932",
"Background_Color_Hover": "#557bb6",
"Border_Color": "#3a7400",
"FontColor": ReturnFontColor(),
},
"ProDark.qss": {
"Background_Color": "#333333",
"Background_Color_Hover": "#557bb6",
"Border_Color": "#adc5ed",
"FontColor": ReturnFontColor(),
},
"Darker.qss": {
"Background_Color": "#444444",
"Background_Color_Hover": "#4aa5ff",
"Border_Color": "#696968",
"FontColor": ReturnFontColor(),
},
"Light-modern.qss": {
"Background_Color": "#f0f0f0",
"Background_Color_Hover": "#4aa5ff",
"Border_Color": "#646464",
"FontColor": ReturnFontColor(),
},
"Dark-modern.qss": {
"Background_Color": "#2b2b2b",
"Background_Color_Hover": "#4aa5ff",
"Border_Color": "#ffffff",
"FontColor": ReturnFontColor(),
},
"Dark-contrast.qss": {
"Background_Color": "#444444",
"Background_Color_Hover": "#4aa5ff",
"Border_Color": "#787878",
"FontColor": ReturnFontColor(),
},
}
}

View File

@ -1,28 +1,34 @@
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<package format="1" xmlns="https://wiki.freecad.org/Package_Metadata">
<name>SearchBar</name>
<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>
<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>
<url type="repository" branch="main">https://github.com/SuzanneSoy/SearchBar</url>
<url type="bugtracker">https://github.com/SuzanneSoy/SearchBar/issues</url>
<url type="documentation">https://github.com/SuzanneSoy/SearchBar</url>
<icon>Tango-System-search.svg</icon>
<url type="repository" branch="main">https://github.com/APEbbers/SearchBar</url>
<url type="bugtracker">https://github.com/APEbbers/SearchBar/issues</url>
<url type="documentation">https://github.com/APEbbers/SearchBar</url>
<depend type="python">lxml</depend>
<content>
<workbench>
<name>SearchBar</name>
<description>Adds a search bar widget for tools, document objects, and preferences</description>
<classname></classname>
<icon>Tango-System-search.svg</icon>
<subdirectory>./</subdirectory>
<tag>search</tag>
<tag>widget</tag>
<tag>ui/ux</tag>
<version>0.1.0</version>
</workbench>
</content>
</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