From bef6f91b3c0e0c2a6c6963a73a8291fa1f490624 Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Thu, 7 Nov 2024 23:16:46 +0100 Subject: [PATCH 01/47] first test --- GetItemGroups.py | 105 +++--- IndentedItemDelegate.py | 18 +- InitGui.py | 58 ++-- RefreshTools.py | 148 +++++---- ResultsDocument.py | 205 +++++++----- ResultsPreferences.py | 146 ++++---- ResultsRefreshTools.py | 37 ++- ResultsToolbar.py | 228 ++++++++----- SafeViewer.py | 188 ++++++----- SearchBox.py | 719 +++++++++++++++++++++------------------- SearchBoxLight.py | 123 ++++--- Serialize.py | 169 ++++++---- 12 files changed, 1222 insertions(+), 922 deletions(-) diff --git a/GetItemGroups.py b/GetItemGroups.py index 42b1bd3..56262b4 100644 --- a/GetItemGroups.py +++ b/GetItemGroups.py @@ -3,55 +3,70 @@ globalGroups = [] itemGroups = None serializedItemGroups = None + def onResultSelected(index, groupId): - global globalGroups - nfo = globalGroups[groupId] - handlerName = nfo['action']['handler'] - import SearchResults - if handlerName in SearchResults.actionHandlers: - SearchResults.actionHandlers[handlerName](nfo) - else: - from PySide import QtGui - QtGui.QMessageBox.warning(None, 'Could not execute this action', 'Could not execute this action, it could be from a Mod that has been uninstalled. Try refreshing the list of tools.') + global globalGroups + nfo = globalGroups[groupId] + handlerName = nfo["action"]["handler"] + import SearchResults + + if handlerName in SearchResults.actionHandlers: + SearchResults.actionHandlers[handlerName](nfo) + else: + from PySide6 import QtGui + + QtGui.QMessageBox.warning( + None, + "Could not execute this action", + "Could not execute this action, it could be from a Mod that has been uninstalled. Try refreshing the list of tools.", + ) + def getToolTip(groupId, setParent): - global globalGroups - nfo = globalGroups[int(groupId)] - handlerName = nfo['action']['handler'] - import SearchResults - if handlerName in SearchResults.toolTipHandlers: - return SearchResults.toolTipHandlers[handlerName](nfo, setParent) - else: - return 'Could not load tooltip for this tool, it could be from a Mod that has been uninstalled. Try refreshing the list of tools.' + global globalGroups + nfo = globalGroups[int(groupId)] + handlerName = nfo["action"]["handler"] + import SearchResults + + if handlerName in SearchResults.toolTipHandlers: + return SearchResults.toolTipHandlers[handlerName](nfo, setParent) + else: + return "Could not load tooltip for this tool, it could be from a Mod that has been uninstalled. Try refreshing the list of tools." + def getItemGroups(): - global itemGroups, serializedItemGroups, globalGroups - - # Import the tooltip+action handlers and search result providers that are bundled with this Mod. - # Other providers should import SearchResults and register their handlers and providers - import BuiltInSearchResults + global itemGroups, serializedItemGroups, globalGroups - # Load the list of tools, preferably from the cache, if it has not already been loaded: - if itemGroups is None: - if serializedItemGroups is None: - import RefreshTools - itemGroups = RefreshTools.refreshToolbars(doLoadAllWorkbenches = False) - else: - import Serialize - itemGroups = Serialize.deserialize(serializedItemGroups) + # Import the tooltip+action handlers and search result providers that are bundled with this Mod. + # Other providers should import SearchResults and register their handlers and providers + import BuiltInSearchResults - # Aggregate the tools (cached) and document objects (not cached), and assign an index to each - import SearchResults - igs = itemGroups - for providerName, provider in SearchResults.resultProvidersUncached.items(): - igs = igs + provider() - globalGroups = [] - def addId(group): - globalGroups.append(group) - group['id'] = len(globalGroups) - 1 - for subitem in group['subitems']: - addId(subitem) - for ig in igs: - addId(ig) - - return igs + # Load the list of tools, preferably from the cache, if it has not already been loaded: + if itemGroups is None: + if serializedItemGroups is None: + import RefreshTools + + itemGroups = RefreshTools.refreshToolbars(doLoadAllWorkbenches=False) + else: + import Serialize + + itemGroups = Serialize.deserialize(serializedItemGroups) + + # Aggregate the tools (cached) and document objects (not cached), and assign an index to each + import SearchResults + + igs = itemGroups + for providerName, provider in SearchResults.resultProvidersUncached.items(): + igs = igs + provider() + globalGroups = [] + + def addId(group): + globalGroups.append(group) + group["id"] = len(globalGroups) - 1 + for subitem in group["subitems"]: + addId(subitem) + + for ig in igs: + addId(ig) + + return igs diff --git a/IndentedItemDelegate.py b/IndentedItemDelegate.py index aff7046..76d2d63 100644 --- a/IndentedItemDelegate.py +++ b/IndentedItemDelegate.py @@ -1,12 +1,14 @@ -from PySide import QtGui +from PySide6 import QtGui + # Inspired by https://stackoverflow.com/a/5443220/324969 # Inspired by https://forum.qt.io/topic/69807/qtreeview-indent-entire-row class IndentedItemDelegate(QtGui.QStyledItemDelegate): - def __init__(self): - super(IndentedItemDelegate, self).__init__() - def paint(self, painter, option, index): - depth = int(option.widget.model().itemData(index.siblingAtColumn(1))[0]) - indent = 16 * depth - option.rect.adjust(indent, 0, 0, 0) - super(IndentedItemDelegate, self).paint(painter, option, index) + def __init__(self): + super(IndentedItemDelegate, self).__init__() + + def paint(self, painter, option, index): + depth = int(option.widget.model().itemData(index.siblingAtColumn(1))[0]) + indent = 16 * depth + option.rect.adjust(indent, 0, 0, 0) + super(IndentedItemDelegate, self).paint(painter, option, index) diff --git a/InitGui.py b/InitGui.py index 94a8b25..bd0a373 100644 --- a/InitGui.py +++ b/InitGui.py @@ -3,35 +3,43 @@ wax = None sea = None tbr = None + def addToolSearchBox(): - import FreeCADGui - from PySide import QtGui - import SearchBoxLight - global wax, sea, tbr - mw = FreeCADGui.getMainWindow() - if mw: - if sea is None: - sea = SearchBoxLight.SearchBoxLight(getItemGroups = lambda: __import__('GetItemGroups').getItemGroups(), - getToolTip = lambda groupId, setParent: __import__('GetItemGroups').getToolTip(groupId, setParent), - getItemDelegate = lambda: __import__('IndentedItemDelegate').IndentedItemDelegate()) - sea.resultSelected.connect(lambda index, groupId: __import__('GetItemGroups').onResultSelected(index, groupId)) + import FreeCADGui + from PySide6 import QtGui + import SearchBoxLight - if wax is None: - wax = QtGui.QWidgetAction(None) - wax.setWhatsThis('Use this search bar to find tools, document objects, preferences and more') + global wax, sea, tbr + mw = FreeCADGui.getMainWindow() + if mw: + if sea is None: + sea = SearchBoxLight.SearchBoxLight( + getItemGroups=lambda: __import__("GetItemGroups").getItemGroups(), + getToolTip=lambda groupId, setParent: __import__("GetItemGroups").getToolTip(groupId, setParent), + getItemDelegate=lambda: __import__("IndentedItemDelegate").IndentedItemDelegate(), + ) + sea.resultSelected.connect( + lambda index, groupId: __import__("GetItemGroups").onResultSelected(index, groupId) + ) + + if wax is None: + wax = QtGui.QWidgetAction(None) + wax.setWhatsThis("Use this search bar to find tools, document objects, preferences and more") + + sea.setWhatsThis("Use this search bar to find tools, document objects, preferences and more") + wax.setDefaultWidget(sea) + ##mbr.addWidget(sea) + # mbr.addAction(wax) + if tbr is None: + tbr = QtGui.QToolBar("SearchBar") # QtGui.QDockWidget() + # Include FreeCAD in the name so that one can find windows labeled with FreeCAD easily in window managers which allow search through the list of open windows. + tbr.setObjectName("SearchBar") + tbr.addAction(wax) + mw.addToolBar(tbr) + tbr.show() - sea.setWhatsThis('Use this search bar to find tools, document objects, preferences and more') - wax.setDefaultWidget(sea) - ##mbr.addWidget(sea) - #mbr.addAction(wax) - if tbr is None: - tbr = QtGui.QToolBar("SearchBar") #QtGui.QDockWidget() - # Include FreeCAD in the name so that one can find windows labeled with FreeCAD easily in window managers which allow search through the list of open windows. - tbr.setObjectName("SearchBar") - tbr.addAction(wax) - mw.addToolBar(tbr) - tbr.show() addToolSearchBox() import FreeCADGui + FreeCADGui.getMainWindow().workbenchActivated.connect(addToolSearchBox) diff --git a/RefreshTools.py b/RefreshTools.py index 1acdad8..ad7c6f9 100644 --- a/RefreshTools.py +++ b/RefreshTools.py @@ -1,81 +1,99 @@ import os import FreeCAD as App + def loadAllWorkbenches(): - from PySide import QtGui - import FreeCADGui - activeWorkbench = FreeCADGui.activeWorkbench().name() - lbl = QtGui.QLabel('Loading workbench … (…/…)') - lbl.show() - lst = FreeCADGui.listWorkbenches() - for i, wb in enumerate(lst): - msg = 'Loading workbench ' + wb + ' (' + str(i) + '/' + str(len(lst)) + ')' - print(msg) - lbl.setText(msg) - geo = lbl.geometry() - geo.setSize(lbl.sizeHint()) - lbl.setGeometry(geo) - lbl.repaint() - FreeCADGui.updateGui() # Probably slower with this, because it redraws the entire GUI with all tool buttons changed etc. but allows the label to actually be updated, and it looks nice and gives a quick overview of all the workbenches… - try: - FreeCADGui.activateWorkbench(wb) - except: - pass - lbl.hide() - FreeCADGui.activateWorkbench(activeWorkbench) + from PySide6 import QtGui + import FreeCADGui + + activeWorkbench = FreeCADGui.activeWorkbench().name() + lbl = QtGui.QLabel("Loading workbench … (…/…)") + lbl.show() + lst = FreeCADGui.listWorkbenches() + for i, wb in enumerate(lst): + msg = "Loading workbench " + wb + " (" + str(i) + "/" + str(len(lst)) + ")" + print(msg) + lbl.setText(msg) + geo = lbl.geometry() + geo.setSize(lbl.sizeHint()) + lbl.setGeometry(geo) + lbl.repaint() + FreeCADGui.updateGui() # Probably slower with this, because it redraws the entire GUI with all tool buttons changed etc. but allows the label to actually be updated, and it looks nice and gives a quick overview of all the workbenches… + try: + FreeCADGui.activateWorkbench(wb) + except: + pass + lbl.hide() + FreeCADGui.activateWorkbench(activeWorkbench) + def cachePath(): - return os.path.join(App.getUserAppDataDir(), 'Cache_SearchBarMod') + return os.path.join(App.getUserAppDataDir(), "Cache_SearchBarMod") + def gatherTools(): - itemGroups = [] - import SearchResults - for providerName, provider in SearchResults.resultProvidersCached.items(): - itemGroups = itemGroups + provider() - return itemGroups + itemGroups = [] + import SearchResults + + for providerName, provider in SearchResults.resultProvidersCached.items(): + itemGroups = itemGroups + provider() + return itemGroups + def writeCacheTools(): - import Serialize - serializedItemGroups = Serialize.serialize(gatherTools()) - # Todo: use wb and a specific encoding. - with open(cachePath(), 'w') as cache: - cache.write(serializedItemGroups) - # I prefer to systematically deserialize, instead of taking the original version, - # this avoids possible inconsistencies between the original and the cache and - # makes sure cache-related bugs are noticed quickly. - import Serialize - itemGroups = Serialize.deserialize(serializedItemGroups) - print('SearchBox: Cache has been written.') - return itemGroups + import Serialize + + serializedItemGroups = Serialize.serialize(gatherTools()) + # Todo: use wb and a specific encoding. + with open(cachePath(), "w") as cache: + cache.write(serializedItemGroups) + # I prefer to systematically deserialize, instead of taking the original version, + # this avoids possible inconsistencies between the original and the cache and + # makes sure cache-related bugs are noticed quickly. + import Serialize + + itemGroups = Serialize.deserialize(serializedItemGroups) + print("SearchBox: Cache has been written.") + return itemGroups + def readCacheTools(): - # Todo: use rb and a specific encoding. - with open(cachePath(), 'r') as cache: - serializedItemGroups = cache.read() - import Serialize - itemGroups = Serialize.deserialize(serializedItemGroups) - print('SearchBox: Tools were loaded from the cache.') - return itemGroups + # Todo: use rb and a specific encoding. + with open(cachePath(), "r") as cache: + serializedItemGroups = cache.read() + import Serialize + + itemGroups = Serialize.deserialize(serializedItemGroups) + print("SearchBox: Tools were loaded from the cache.") + return itemGroups -def refreshToolbars(doLoadAllWorkbenches = True): - if doLoadAllWorkbenches: - loadAllWorkbenches() - return writeCacheTools() - else: - try: - return readCacheTools() - except: - return writeCacheTools() +def refreshToolbars(doLoadAllWorkbenches=True): + if doLoadAllWorkbenches: + loadAllWorkbenches() + return writeCacheTools() + else: + try: + return readCacheTools() + except: + return writeCacheTools() + def refreshToolsAction(): - from PySide import QtGui - print('Refresh list of tools') - fw = QtGui.QApplication.focusWidget() - if fw is not None: - fw.clearFocus() - reply = QtGui.QMessageBox.question(None, "Load all workbenches?", "Load all workbenches? This can cause FreeCAD to become unstable, and this \"reload tools\" feature contained a bug that crashed freecad systematically, so please make sure you save your work before. It's a good idea to restart FreeCAD after this operation.", QtGui.QMessageBox.Yes, QtGui.QMessageBox.No) - if reply == QtGui.QMessageBox.Yes: - refreshToolbars() - else: - print('cancelled') + from PySide6 import QtGui + + print("Refresh list of tools") + fw = QtGui.QApplication.focusWidget() + if fw is not None: + fw.clearFocus() + reply = QtGui.QMessageBox.question( + None, + "Load all workbenches?", + 'Load all workbenches? This can cause FreeCAD to become unstable, and this "reload tools" feature contained a bug that crashed freecad systematically, so please make sure you save your work before. It\'s a good idea to restart FreeCAD after this operation.', + QtGui.QMessageBox.Yes, + QtGui.QMessageBox.No, + ) + if reply == QtGui.QMessageBox.Yes: + refreshToolbars() + else: + print("cancelled") diff --git a/ResultsDocument.py b/ResultsDocument.py index e726594..44435b1 100644 --- a/ResultsDocument.py +++ b/ResultsDocument.py @@ -1,116 +1,143 @@ -from PySide import QtGui -from PySide import QtCore +from PySide6 import QtGui +from PySide6 import QtCore import FreeCAD as App import FreeCADGui import SafeViewer import SearchBox + def documentAction(nfo): - act = nfo['action'] + act = nfo["action"] # Todo: this should also select the document in the tree view - print('switch to document ' + act['document']) - App.setActiveDocument(act['document']) + print("switch to document " + act["document"]) + App.setActiveDocument(act["document"]) + def documentObjectAction(nfo): - act = nfo['action'] - print('select object ' + act['document'] + '.' + act['object']) - FreeCADGui.Selection.addSelection(act['document'], act['object']) + act = nfo["action"] + print("select object " + act["document"] + "." + act["object"]) + FreeCADGui.Selection.addSelection(act["document"], act["object"]) + # For some reason, the viewer always works except when used for two consecutive items in the search results: it then disappears after a short zoom-in+zoom-out animation. # I'm giving up on getting this viewer to work in a clean way, and will try swapping two instances so that the same one is never used twice in a row. # Also, in order to avoid segfaults when the module is reloaded (which causes the previous viewer to be garbage collected at some point), we're using a global property that will survive module reloads. -if not hasattr(App, '_SearchBar3DViewer'): - # Toggle between - App._SearchBar3DViewer = None - App._SearchBar3DViewerB = None +if not hasattr(App, "_SearchBar3DViewer"): + # Toggle between + App._SearchBar3DViewer = None + App._SearchBar3DViewerB = None + class DocumentObjectToolTipWidget(QtGui.QWidget): - def __init__(self, nfo, setParent): - import pivy - super(DocumentObjectToolTipWidget, self).__init__() - html = '

' + nfo['toolTip']['label'] + '

App.getDocument(' + repr(str(nfo['toolTip']['docName'])) + ').getObject(' + repr(str(nfo['toolTip']['name'])) + ')

' - description = QtGui.QTextEdit() - description.setReadOnly(True) - description.setAlignment(QtCore.Qt.AlignTop) - description.setText(html) + def __init__(self, nfo, setParent): + import pivy - if App._SearchBar3DViewer is None: - oldFocus = QtGui.QApplication.focusWidget() - SearchBox.globalIgnoreFocusOut - SearchBox.globalIgnoreFocusOut = True - App._SearchBar3DViewer = SafeViewer.SafeViewer() - App._SearchBar3DViewerB = SafeViewer.SafeViewer() - oldFocus.setFocus() - SearchBox.globalIgnoreFocusOut = False - # Tried setting the preview to a fixed size to prevent it from disappearing when changing its contents, this sets it to a fixed size but doesn't actually pick the size, .resize does that but isn't enough to fix the bug. - #safeViewerInstance.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed)) - self.preview = App._SearchBar3DViewer - App._SearchBar3DViewer, App._SearchBar3DViewerB = App._SearchBar3DViewerB, App._SearchBar3DViewer + super(DocumentObjectToolTipWidget, self).__init__() + html = ( + "

" + + nfo["toolTip"]["label"] + + "

App.getDocument(" + + repr(str(nfo["toolTip"]["docName"])) + + ").getObject(" + + repr(str(nfo["toolTip"]["name"])) + + ")

" + ) + description = QtGui.QTextEdit() + description.setReadOnly(True) + description.setAlignment(QtCore.Qt.AlignTop) + description.setText(html) - obj = App.getDocument(str(nfo['toolTip']['docName'])).getObject(str(nfo['toolTip']['name'])) + if App._SearchBar3DViewer is None: + oldFocus = QtGui.QApplication.focusWidget() + SearchBox.globalIgnoreFocusOut + SearchBox.globalIgnoreFocusOut = True + App._SearchBar3DViewer = SafeViewer.SafeViewer() + App._SearchBar3DViewerB = SafeViewer.SafeViewer() + oldFocus.setFocus() + SearchBox.globalIgnoreFocusOut = False + # Tried setting the preview to a fixed size to prevent it from disappearing when changing its contents, this sets it to a fixed size but doesn't actually pick the size, .resize does that but isn't enough to fix the bug. + # safeViewerInstance.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed)) + self.preview = App._SearchBar3DViewer + App._SearchBar3DViewer, App._SearchBar3DViewerB = App._SearchBar3DViewerB, App._SearchBar3DViewer - # This is really a bad way to do this… to prevent the setExtraInfo function from - # finalizing the object, we remove the parent ourselves. - oldParent = self.preview.parent() - lay = QtGui.QVBoxLayout() - lay.setContentsMargins(0,0,0,0) - lay.setSpacing(0) - self.setLayout(lay) - lay.addWidget(description) - lay.addWidget(self.preview) - #if oldParent is not None: - # oldParent.hide() # hide before detaching, or we have widgets floating as their own window that appear for a split second in some cases. - # oldParent.setParent(None) - - # Tried hiding/detaching the preview to prevent it from disappearing when changing its contents - #self.preview.viewer.stopAnimating() - self.preview.showSceneGraph(obj.ViewObject.RootNode) + obj = App.getDocument(str(nfo["toolTip"]["docName"])).getObject(str(nfo["toolTip"]["name"])) - setParent(self) - # Let the GUI recompute the side of the description based on its horizontal size. - FreeCADGui.updateGui() - siz = description.document().size().toSize() - description.setFixedHeight(siz.height() + 5) + # This is really a bad way to do this… to prevent the setExtraInfo function from + # finalizing the object, we remove the parent ourselves. + oldParent = self.preview.parent() + lay = QtGui.QVBoxLayout() + lay.setContentsMargins(0, 0, 0, 0) + lay.setSpacing(0) + self.setLayout(lay) + lay.addWidget(description) + lay.addWidget(self.preview) + # if oldParent is not None: + # oldParent.hide() # hide before detaching, or we have widgets floating as their own window that appear for a split second in some cases. + # oldParent.setParent(None) + + # Tried hiding/detaching the preview to prevent it from disappearing when changing its contents + # self.preview.viewer.stopAnimating() + self.preview.showSceneGraph(obj.ViewObject.RootNode) + + setParent(self) + # Let the GUI recompute the side of the description based on its horizontal size. + FreeCADGui.updateGui() + siz = description.document().size().toSize() + description.setFixedHeight(siz.height() + 5) + + def finalizer(self): + # self.preview.finalizer() + # Detach the widget so that it may be reused without getting deleted + self.preview.setParent(None) - def finalizer(self): - #self.preview.finalizer() - # Detach the widget so that it may be reused without getting deleted - self.preview.setParent(None) def documentToolTip(nfo, setParent): - return '

' + nfo['toolTip']['label'] + '

App.getDocument(' + repr(str(nfo['toolTip']['name'])) + ')

' + return ( + "

" + + nfo["toolTip"]["label"] + + "

App.getDocument(" + + repr(str(nfo["toolTip"]["name"])) + + ')

' + ) + def documentObjectToolTip(nfo, setParent): - return DocumentObjectToolTipWidget(nfo, setParent) + return DocumentObjectToolTipWidget(nfo, setParent) + def documentResultsProvider(): - itemGroups = [] - def document(doc): - group = [] - for o in doc.Objects: - #all_actions.append(lambda: ) - action = { 'handler': 'documentObject', 'document': o.Document.Name, 'object': o.Name } - item = { - 'icon': o.ViewObject.Icon if o.ViewObject and o.ViewObject.Icon else None, - 'text': o.Label + ' (' + o.Name + ')', - # TODO: preview of the object - 'toolTip': { 'label': o.Label, 'name': o.Name, 'docName': o.Document.Name}, - 'action': action, - 'subitems': [] - } - group.append(item) + itemGroups = [] - action = { 'handler': 'document', 'document': doc.Name } - itemGroups.append({ - 'icon': QtGui.QIcon(':/icons/Document.svg'), - 'text': doc.Label + ' (' + doc.Name + ')', - # TODO: preview of the document - 'toolTip': { 'label': doc.Label, 'name': doc.Name}, - 'action':action, - 'subitems': group }) - if App.ActiveDocument: - document(App.ActiveDocument) - for docname, doc in App.listDocuments().items(): - if not App.activeDocument or docname != App.ActiveDocument.Name: - document(doc) - return itemGroups + def document(doc): + group = [] + for o in doc.Objects: + # all_actions.append(lambda: ) + action = {"handler": "documentObject", "document": o.Document.Name, "object": o.Name} + item = { + "icon": o.ViewObject.Icon if o.ViewObject and o.ViewObject.Icon else None, + "text": o.Label + " (" + o.Name + ")", + # TODO: preview of the object + "toolTip": {"label": o.Label, "name": o.Name, "docName": o.Document.Name}, + "action": action, + "subitems": [], + } + group.append(item) + + action = {"handler": "document", "document": doc.Name} + itemGroups.append( + { + "icon": QtGui.QIcon(":/icons/Document.svg"), + "text": doc.Label + " (" + doc.Name + ")", + # TODO: preview of the document + "toolTip": {"label": doc.Label, "name": doc.Name}, + "action": action, + "subitems": group, + } + ) + + if App.ActiveDocument: + document(App.ActiveDocument) + for docname, doc in App.listDocuments().items(): + if not App.activeDocument or docname != App.ActiveDocument.Name: + document(doc) + return itemGroups diff --git a/ResultsPreferences.py b/ResultsPreferences.py index 737e9b0..3ba67f4 100644 --- a/ResultsPreferences.py +++ b/ResultsPreferences.py @@ -1,90 +1,122 @@ import os import FreeCAD as App import FreeCADGui -from PySide import QtGui +from PySide6 import QtGui import Serialize -genericToolIcon = QtGui.QIcon(QtGui.QIcon(os.path.dirname(__file__) + '/Tango-Tools-spanner-hammer.svg')) +genericToolIcon = QtGui.QIcon(QtGui.QIcon(os.path.dirname(__file__) + "/Tango-Tools-spanner-hammer.svg")) + def getParam(grpPath, type_, name): - return { - # TODO: use letter icon based on the type, as the preferences editor does - 'icon': genericToolIcon, - 'text': name, - 'toolTip': '', - 'action': {'handler': 'param', 'path': grpPath, 'type': type_, 'name': name}, - 'subitems': [] - } + return { + # TODO: use letter icon based on the type, as the preferences editor does + "icon": genericToolIcon, + "text": name, + "toolTip": "", + "action": {"handler": "param", "path": grpPath, "type": type_, "name": name}, + "subitems": [], + } + def getParamGroup(grpPath): - try: - grp = App.ParamGet(grpPath) - except: - return [] - contents = grp.GetContents() - if contents is not None: - return [getParam(grpPath, type_, name) for (type_, name, value) in contents] - else: - return [] + try: + grp = App.ParamGet(grpPath) + except: + return [] + contents = grp.GetContents() + if contents is not None: + return [getParam(grpPath, type_, name) for (type_, name, value) in contents] + else: + return [] + def getParamGroups(nameInConfig, nameInPath): userParameterPath = App.ConfigGet(nameInConfig) from lxml import etree + xml = etree.parse(userParameterPath).getroot() xml.find('FCParamGroup[@Name="Root"]') root = xml.find('FCParamGroup[@Name="Root"]') + def recur(atRoot, path, name, tree): - params = [] if atRoot else getParamGroup(path) - subgroups = [recur(False, path + (':' if atRoot else '/') + child.attrib['Name'], child.attrib['Name'], child) for child in tree.getchildren() if child.tag == 'FCParamGroup'] - return { - 'icon': QtGui.QIcon(':/icons/Group.svg'), - 'text': name, - 'toolTip': '', - 'action': { 'handler': 'paramGroup', 'path': path, 'name': name }, - 'subitems': params + subgroups - } + params = [] if atRoot else getParamGroup(path) + subgroups = [ + recur(False, path + (":" if atRoot else "/") + child.attrib["Name"], child.attrib["Name"], child) + for child in tree.getchildren() + if child.tag == "FCParamGroup" + ] + return { + "icon": QtGui.QIcon(":/icons/Group.svg"), + "text": name, + "toolTip": "", + "action": {"handler": "paramGroup", "path": path, "name": name}, + "subitems": params + subgroups, + } + return recur(True, nameInPath, nameInPath, root) + def getAllParams(): - try: - from lxml import etree - return [getParamGroups('UserParameter', 'User parameter')] - except: - print('Could not load the list of all parameters. Please install the LXML python library with:\npython -m pip install --upgrade lxml') - return [] + try: + from lxml import etree + + return [getParamGroups("UserParameter", "User parameter")] + except: + print( + "Could not load the list of all parameters. Please install the LXML python library with:\npython -m pip install --upgrade lxml" + ) + return [] + def paramGroupAction(nfo): - FreeCADGui.runCommand('Std_DlgParameter',0) - print('Open Parameter Editor (parameter group)') - # TODO: find a way to select the desired group in the parameter dialog once it opens + FreeCADGui.runCommand("Std_DlgParameter", 0) + print("Open Parameter Editor (parameter group)") + # TODO: find a way to select the desired group in the parameter dialog once it opens + def paramAction(nfo): - FreeCADGui.runCommand('Std_DlgParameter',0) - print('Open Parameter Editor (single parameter)') - # TODO: find a way to select the desired parameter in the parameter dialog once it opens + FreeCADGui.runCommand("Std_DlgParameter", 0) + print("Open Parameter Editor (single parameter)") + # TODO: find a way to select the desired parameter in the parameter dialog once it opens + getters = { - 'Boolean' : 'GetBool', - 'Float' : 'GetFloat', - 'Integer' : 'GetInt', - 'String' : 'GetString', - 'Unsigned Long': 'GetUnsigned', + "Boolean": "GetBool", + "Float": "GetFloat", + "Integer": "GetInt", + "String": "GetString", + "Unsigned Long": "GetUnsigned", } + def paramGroupToolTip(nfo, setParent): - path = nfo['action']['path'] - name = nfo['action']['name'] - return '

' + name + '

App.ParamGet(' + repr(path) + ')" + name + "

App.ParamGet(" + repr(path) + ")App.ParamGet(' + repr(path) + ').' + getters[type_] + '(' + repr(name) + ')

Type: ' + type_ + '

Value: ' + repr(value) + '

' + 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 ( + "

App.ParamGet(" + + repr(path) + + ")." + + getters[type_] + + "(" + + repr(name) + + ")

Type: " + + type_ + + "

Value: " + + repr(value) + + "

" + ) + def paramResultsProvider(): - return getAllParams() + return getAllParams() diff --git a/ResultsRefreshTools.py b/ResultsRefreshTools.py index 74859a9..07f7915 100644 --- a/ResultsRefreshTools.py +++ b/ResultsRefreshTools.py @@ -1,22 +1,31 @@ import os -from PySide import QtGui +from PySide6 import QtGui import Serialize + def refreshToolsAction(nfo): - import RefreshTools - RefreshTools.refreshToolsAction() + import RefreshTools + + RefreshTools.refreshToolsAction() + def refreshToolsToolTip(nfo, setParent): - return Serialize.iconToHTML(genericToolIcon) + '

Load all workbenches to refresh this list of tools. This may take a minute, depending on the number of installed workbenches.

' + return ( + Serialize.iconToHTML(genericToolIcon) + + "

Load all workbenches to refresh this list of tools. This may take a minute, depending on the number of installed workbenches.

" + ) + + +genericToolIcon = QtGui.QIcon(QtGui.QIcon(os.path.dirname(__file__) + "/Tango-Tools-spanner-hammer.svg")) + -genericToolIcon = QtGui.QIcon(QtGui.QIcon(os.path.dirname(__file__) + '/Tango-Tools-spanner-hammer.svg')) def refreshToolsResultsProvider(): - return [ - { - 'icon': genericToolIcon, - 'text': 'Refresh list of tools', - 'toolTip': '', - 'action': {'handler': 'refreshTools'}, - 'subitems': [] - } - ] \ No newline at end of file + return [ + { + "icon": genericToolIcon, + "text": "Refresh list of tools", + "toolTip": "", + "action": {"handler": "refreshTools"}, + "subitems": [], + } + ] diff --git a/ResultsToolbar.py b/ResultsToolbar.py index d0ab287..be61be6 100644 --- a/ResultsToolbar.py +++ b/ResultsToolbar.py @@ -1,105 +1,153 @@ -from PySide import QtGui +from PySide6 import QtGui import FreeCADGui import Serialize + def toolbarAction(nfo): - act = nfo['action'] - print('show toolbar ' + act['toolbar'] + ' from workbenches ' + repr(act['workbenches'])) + act = nfo["action"] + print("show toolbar " + act["toolbar"] + " from workbenches " + repr(act["workbenches"])) + def subToolAction(nfo): - act = nfo['action'] - toolPath = act['toolbar'] + '.' + act['tool'] - if 'subTool' in act: - toolPath = toolPath + '.' + act['subTool'] - def runTool(): - mw = FreeCADGui.getMainWindow() - for the_toolbar in mw.findChildren(QtGui.QToolBar, act['toolbar']): - for tbt in the_toolbar.findChildren(QtGui.QToolButton): - if tbt.text() == act['tool']: - action = None - if 'subTool' in act: - men = tbt.menu() - if men: - for mac in men.actions(): - if mac.text() == act['subTool']: - action = mac - break - else: - action = tbt.defaultAction() - if 'showMenu' in act and act['showMenu']: - print('Popup submenu of tool ' + toolPath + ' available in workbenches ' + repr(act['workbenches'])) - the_toolbar.show() - tbt.showMenu() - return True - elif action is not None: - print('Run action of tool ' + toolPath + ' available in workbenches ' + repr(act['workbenches'])) - action.trigger() - return True - return False - if runTool(): - return - else: - for workbench in act['workbenches']: - print('Activating workbench ' + workbench + ' to access tool ' + toolPath) - FreeCADGui.activateWorkbench(workbench) - if runTool(): + act = nfo["action"] + toolPath = act["toolbar"] + "." + act["tool"] + if "subTool" in act: + toolPath = toolPath + "." + act["subTool"] + + def runTool(): + mw = FreeCADGui.getMainWindow() + for the_toolbar in mw.findChildren(QtGui.QToolBar, act["toolbar"]): + for tbt in the_toolbar.findChildren(QtGui.QToolButton): + if tbt.text() == act["tool"]: + action = None + if "subTool" in act: + men = tbt.menu() + if men: + for mac in men.actions(): + if mac.text() == act["subTool"]: + action = mac + break + else: + action = tbt.defaultAction() + if "showMenu" in act and act["showMenu"]: + print( + "Popup submenu of tool " + + toolPath + + " available in workbenches " + + repr(act["workbenches"]) + ) + the_toolbar.show() + tbt.showMenu() + return True + elif action is not None: + print( + "Run action of tool " + toolPath + " available in workbenches " + repr(act["workbenches"]) + ) + action.trigger() + return True + return False + + if runTool(): return - print('Tool ' + toolPath + ' not found, was it offered by an extension that is no longer present?') + else: + for workbench in act["workbenches"]: + print("Activating workbench " + workbench + " to access tool " + toolPath) + FreeCADGui.activateWorkbench(workbench) + if runTool(): + return + print("Tool " + toolPath + " not found, was it offered by an extension that is no longer present?") + def toolbarToolTip(nfo, setParent): - workbenches = FreeCADGui.listWorkbenches() - in_workbenches = ['
  • ' + (Serialize.iconToHTML(QtGui.QIcon(workbenches[wb].Icon)) if wb in workbenches else '? ') + wb + '
  • ' for wb in nfo['action']['workbenches']] - return '

    Show the ' + nfo['text'] + ' toolbar

    This toolbar appears in the following workbenches:

    ' + workbenches = FreeCADGui.listWorkbenches() + in_workbenches = [ + "
  • " + (Serialize.iconToHTML(QtGui.QIcon(workbenches[wb].Icon)) if wb in workbenches else "? ") + wb + "
  • " + for wb in nfo["action"]["workbenches"] + ] + return ( + "

    Show the " + + nfo["text"] + + " toolbar

    This toolbar appears in the following workbenches:

    " + ) + def subToolToolTip(nfo, setParent): - return Serialize.iconToHTML(nfo['icon'], 32) + '

    ' + nfo['toolTip'] + '

    ' + return Serialize.iconToHTML(nfo["icon"], 32) + "

    " + nfo["toolTip"] + "

    " + def getAllToolbars(): - all_tbs = dict() - for wbname, workbench in FreeCADGui.listWorkbenches().items(): - try: - tbs = workbench.listToolbars() - except: - continue - # careful, tbs contains all the toolbars of the workbench, including shared toolbars - for tb in tbs: - if tb not in all_tbs: - all_tbs[tb] = set() - all_tbs[tb].add(wbname) - return all_tbs + all_tbs = dict() + for wbname, workbench in FreeCADGui.listWorkbenches().items(): + try: + tbs = workbench.listToolbars() + except: + continue + # careful, tbs contains all the toolbars of the workbench, including shared toolbars + for tb in tbs: + if tb not in all_tbs: + all_tbs[tb] = set() + all_tbs[tb].add(wbname) + return all_tbs + def toolbarResultsProvider(): - itemGroups = [] - all_tbs = getAllToolbars() - mw = FreeCADGui.getMainWindow() - for toolbarName, toolbarIsInWorkbenches in all_tbs.items(): - toolbarIsInWorkbenches = sorted(list(toolbarIsInWorkbenches)) - for the_toolbar in mw.findChildren(QtGui.QToolBar, toolbarName): - group = [] - for tbt in the_toolbar.findChildren(QtGui.QToolButton): - text = tbt.text() - act = tbt.defaultAction() - if text != '': - # TODO: there also is the tooltip - icon = tbt.icon() - men = tbt.menu() - subgroup = [] - if men: - subgroup = [] - for mac in men.actions(): - if mac.text(): - action = { 'handler': 'subTool', 'workbenches': toolbarIsInWorkbenches, 'toolbar': toolbarName, 'tool': text, 'subTool': mac.text() } - subgroup.append({'icon':mac.icon(), 'text':mac.text(), 'toolTip': mac.toolTip(), 'action':action, 'subitems':[]}) - # The default action of a menu changes dynamically, instead of triggering the last action, just show the menu. - action = { 'handler': 'tool', 'workbenches': toolbarIsInWorkbenches, 'toolbar': toolbarName, 'tool': text, 'showMenu': bool(men) } - group.append({'icon':icon, 'text':text, 'toolTip': tbt.toolTip(), 'action': action, 'subitems': subgroup}) - # TODO: move the 'workbenches' field to the itemgroup - action = { 'handler': 'toolbar', 'workbenches': toolbarIsInWorkbenches, 'toolbar': toolbarName } - itemGroups.append({ - 'icon': QtGui.QIcon(':/icons/Group.svg'), - 'text': toolbarName, - 'toolTip': '', - 'action': action, - 'subitems': group - }) - return itemGroups + itemGroups = [] + all_tbs = getAllToolbars() + mw = FreeCADGui.getMainWindow() + for toolbarName, toolbarIsInWorkbenches in all_tbs.items(): + toolbarIsInWorkbenches = sorted(list(toolbarIsInWorkbenches)) + for the_toolbar in mw.findChildren(QtGui.QToolBar, toolbarName): + group = [] + for tbt in the_toolbar.findChildren(QtGui.QToolButton): + text = tbt.text() + act = tbt.defaultAction() + if text != "": + # TODO: there also is the tooltip + icon = tbt.icon() + men = tbt.menu() + subgroup = [] + if men: + subgroup = [] + for mac in men.actions(): + if mac.text(): + action = { + "handler": "subTool", + "workbenches": toolbarIsInWorkbenches, + "toolbar": toolbarName, + "tool": text, + "subTool": mac.text(), + } + subgroup.append( + { + "icon": mac.icon(), + "text": mac.text(), + "toolTip": mac.toolTip(), + "action": action, + "subitems": [], + } + ) + # The default action of a menu changes dynamically, instead of triggering the last action, just show the menu. + action = { + "handler": "tool", + "workbenches": toolbarIsInWorkbenches, + "toolbar": toolbarName, + "tool": text, + "showMenu": bool(men), + } + group.append( + {"icon": icon, "text": text, "toolTip": tbt.toolTip(), "action": action, "subitems": subgroup} + ) + # TODO: move the 'workbenches' field to the itemgroup + action = {"handler": "toolbar", "workbenches": toolbarIsInWorkbenches, "toolbar": toolbarName} + itemGroups.append( + { + "icon": QtGui.QIcon(":/icons/Group.svg"), + "text": toolbarName, + "toolTip": "", + "action": action, + "subitems": group, + } + ) + return itemGroups diff --git a/SafeViewer.py b/SafeViewer.py index 8381c20..37973ef 100644 --- a/SafeViewer.py +++ b/SafeViewer.py @@ -1,110 +1,120 @@ -from PySide import QtGui +from PySide6 import QtGui import FreeCAD + class SafeViewer(QtGui.QWidget): - """FreeCAD uses a modified version of QuarterWidget, so the import pivy.quarter one will cause segfaults. - FreeCAD's FreeCADGui.createViewer() puts the viewer widget inside an MDI window, and detaching it without causing segfaults on exit is tricky. - This class contains some kludges to extract the viewer as a standalone widget and destroy it safely.""" - enabled = FreeCAD.ParamGet('User parameter:BaseApp/Preferences/Mod/SearchBar').GetBool('PreviewEnabled', False) - instances = [] - def __init__(self, parent = None): - super(SafeViewer, self).__init__() - SafeViewer.instances.append(self) - self.init_parent = parent - self.instance_enabled = False # Has this specific instance been enabled? - if SafeViewer.enabled: - self.displaying_warning = False - self.enable() - else: - import FreeCADGui - from PySide import QtCore - self.displaying_warning = True - self.lbl_warning = QtGui.QTextEdit() - self.lbl_warning.setReadOnly(True) - self.lbl_warning.setAlignment(QtCore.Qt.AlignTop) - self.lbl_warning.setText("Warning: the 3D preview has some stability issues. It can cause FreeCAD to crash (usually when quitting the application) and could in theory cause data loss, inside and outside of FreeCAD.") - self.btn_enable_for_this_session = QtGui.QPushButton('Enable 3D preview for this session') - self.btn_enable_for_this_session.clicked.connect(self.enable_for_this_session) - self.btn_enable_for_future_sessions = QtGui.QPushButton('Enable 3D preview for future sessions') - self.btn_enable_for_future_sessions.clicked.connect(self.enable_for_future_sessions) - self.setLayout(QtGui.QVBoxLayout()) - self.layout().addWidget(self.lbl_warning) - self.layout().addWidget(self.btn_enable_for_this_session) - self.layout().addWidget(self.btn_enable_for_future_sessions) - - def enable_for_this_session(self): - if not SafeViewer.enabled: - for instance in SafeViewer.instances: - instance.enable() + """FreeCAD uses a modified version of QuarterWidget, so the import pivy.quarter one will cause segfaults. + FreeCAD's FreeCADGui.createViewer() puts the viewer widget inside an MDI window, and detaching it without causing segfaults on exit is tricky. + This class contains some kludges to extract the viewer as a standalone widget and destroy it safely.""" - def enable_for_future_sessions(self): - if not SafeViewer.enabled: - # Store in prefs - FreeCAD.ParamGet('User parameter:BaseApp/Preferences/Mod/SearchBar').SetBool('PreviewEnabled', True) - # Then enable as usual - self.enable_for_this_session() + enabled = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/SearchBar").GetBool("PreviewEnabled", False) + instances = [] - def enable(self): - if not self.instance_enabled: - import FreeCADGui - # TODO: use a mutex wrapping the entire method, if possible - SafeViewer.enabled = True - self.instance_enabled = True # Has this specific instance been enabled? + def __init__(self, parent=None): + super(SafeViewer, self).__init__() + SafeViewer.instances.append(self) + self.init_parent = parent + self.instance_enabled = False # Has this specific instance been enabled? + if SafeViewer.enabled: + self.displaying_warning = False + self.enable() + else: + import FreeCADGui + from PySide6 import QtCore - if (self.displaying_warning): - self.layout().removeWidget(self.lbl_warning) - self.layout().removeWidget(self.btn_enable_for_this_session) - self.layout().removeWidget(self.btn_enable_for_future_sessions) + self.displaying_warning = True + self.lbl_warning = QtGui.QTextEdit() + self.lbl_warning.setReadOnly(True) + self.lbl_warning.setAlignment(QtCore.Qt.AlignTop) + self.lbl_warning.setText( + "Warning: the 3D preview has some stability issues. It can cause FreeCAD to crash (usually when quitting the application) and could in theory cause data loss, inside and outside of FreeCAD." + ) + self.btn_enable_for_this_session = QtGui.QPushButton("Enable 3D preview for this session") + self.btn_enable_for_this_session.clicked.connect(self.enable_for_this_session) + self.btn_enable_for_future_sessions = QtGui.QPushButton("Enable 3D preview for future sessions") + self.btn_enable_for_future_sessions.clicked.connect(self.enable_for_future_sessions) + self.setLayout(QtGui.QVBoxLayout()) + self.layout().addWidget(self.lbl_warning) + self.layout().addWidget(self.btn_enable_for_this_session) + self.layout().addWidget(self.btn_enable_for_future_sessions) - self.viewer = FreeCADGui.createViewer() - self.graphicsView = self.viewer.graphicsView() - self.oldGraphicsViewParent = self.graphicsView.parent() - self.oldGraphicsViewParentParent = self.oldGraphicsViewParent.parent() - self.oldGraphicsViewParentParentParent = self.oldGraphicsViewParentParent.parent() + def enable_for_this_session(self): + if not SafeViewer.enabled: + for instance in SafeViewer.instances: + instance.enable() - # Avoid segfault but still hide the undesired window by moving it to a new hidden MDI area. - self.hiddenQMDIArea = QtGui.QMdiArea() - self.hiddenQMDIArea.addSubWindow(self.oldGraphicsViewParentParentParent) + def enable_for_future_sessions(self): + if not SafeViewer.enabled: + # Store in prefs + FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/SearchBar").SetBool("PreviewEnabled", True) + # Then enable as usual + self.enable_for_this_session() - self.private_widget = self.oldGraphicsViewParent - self.private_widget.setParent(self.init_parent) + def enable(self): + if not self.instance_enabled: + import FreeCADGui - self.setLayout(QtGui.QVBoxLayout()) - self.layout().addWidget(self.private_widget) - self.layout().setContentsMargins(0,0,0,0) + # TODO: use a mutex wrapping the entire method, if possible + SafeViewer.enabled = True + self.instance_enabled = True # Has this specific instance been enabled? - def fin(slf): - slf.finalizer() + if self.displaying_warning: + self.layout().removeWidget(self.lbl_warning) + self.layout().removeWidget(self.btn_enable_for_this_session) + self.layout().removeWidget(self.btn_enable_for_future_sessions) - import weakref - weakref.finalize(self, fin, self) + self.viewer = FreeCADGui.createViewer() + self.graphicsView = self.viewer.graphicsView() + self.oldGraphicsViewParent = self.graphicsView.parent() + self.oldGraphicsViewParentParent = self.oldGraphicsViewParent.parent() + self.oldGraphicsViewParentParentParent = self.oldGraphicsViewParentParent.parent() - self.destroyed.connect(self.finalizer) + # Avoid segfault but still hide the undesired window by moving it to a new hidden MDI area. + self.hiddenQMDIArea = QtGui.QMdiArea() + self.hiddenQMDIArea.addSubWindow(self.oldGraphicsViewParentParentParent) - def finalizer(self): - # Cleanup in an order that doesn't cause a segfault: - if SafeViewer.enabled: - self.private_widget.setParent(self.oldGraphicsViewParentParent) - self.oldGraphicsViewParentParentParent.close() - self.oldGraphicsViewParentParentParent = None - self.oldGraphicsViewParentParent = None - self.oldGraphicsViewParent = None - self.graphicsView = None - self.viewer = None - #self.parent = None - self.init_parent = None - self.hiddenQMDIArea = None + self.private_widget = self.oldGraphicsViewParent + self.private_widget.setParent(self.init_parent) + + self.setLayout(QtGui.QVBoxLayout()) + self.layout().addWidget(self.private_widget) + self.layout().setContentsMargins(0, 0, 0, 0) + + def fin(slf): + slf.finalizer() + + import weakref + + weakref.finalize(self, fin, self) + + self.destroyed.connect(self.finalizer) + + def finalizer(self): + # Cleanup in an order that doesn't cause a segfault: + if SafeViewer.enabled: + self.private_widget.setParent(self.oldGraphicsViewParentParent) + self.oldGraphicsViewParentParentParent.close() + self.oldGraphicsViewParentParentParent = None + self.oldGraphicsViewParentParent = None + self.oldGraphicsViewParent = None + self.graphicsView = None + self.viewer = None + # self.parent = None + self.init_parent = None + self.hiddenQMDIArea = None + + def showSceneGraph(self, g): + import FreeCAD as App + + if SafeViewer.enabled: + self.viewer.getViewer().setSceneGraph(g) + self.viewer.setCameraOrientation(App.Rotation(1, 1, 0, 0.2)) + self.viewer.fitAll() - def showSceneGraph(self, g): - import FreeCAD as App - if SafeViewer.enabled: - self.viewer.getViewer().setSceneGraph(g) - self.viewer.setCameraOrientation(App.Rotation(1,1,0, 0.2)) - self.viewer.fitAll() """ # Example use: -from PySide import QtGui +from PySide6 import QtGui import pivy from SafeViewer import SafeViewer sv = SafeViewer() diff --git a/SearchBox.py b/SearchBox.py index fd3cf77..4ffe46c 100644 --- a/SearchBox.py +++ b/SearchBox.py @@ -1,35 +1,38 @@ import os -from PySide import QtGui -from PySide import QtCore -import FreeCADGui # just used for FreeCADGui.updateGui() +from PySide6 import QtGui +from PySide6 import QtCore +import FreeCADGui # just used for FreeCADGui.updateGui() from SearchBoxLight import SearchBoxLight globalIgnoreFocusOut = False -genericToolIcon = QtGui.QIcon(QtGui.QIcon(os.path.dirname(__file__) + '/Tango-Tools-spanner-hammer.svg')) +genericToolIcon = QtGui.QIcon(QtGui.QIcon(os.path.dirname(__file__) + "/Tango-Tools-spanner-hammer.svg")) + def easyToolTipWidget(html): - foo = QtGui.QTextEdit() - foo.setReadOnly(True) - foo.setAlignment(QtCore.Qt.AlignTop) - foo.setText(html) - return foo + foo = QtGui.QTextEdit() + foo.setReadOnly(True) + foo.setAlignment(QtCore.Qt.AlignTop) + foo.setText(html) + return foo + class SearchBox(QtGui.QLineEdit): - # The following block of code is present in the lightweight proxy SearchBoxLight - ''' - resultSelected = QtCore.Signal(int, int) - ''' - @staticmethod - def lazyInit(self): - if self.isInitialized: - return self - getItemGroups = self.getItemGroups - getToolTip = self.getToolTip - getItemDelegate = self.getItemDelegate - maxVisibleRows = self.maxVisibleRows - # The following block of code is executed by the lightweight proxy SearchBoxLight - ''' + # The following block of code is present in the lightweight proxy SearchBoxLight + """ + resultSelected = QtCore.Signal(int, int) + """ + + @staticmethod + def lazyInit(self): + if self.isInitialized: + return self + getItemGroups = self.getItemGroups + getToolTip = self.getToolTip + getItemDelegate = self.getItemDelegate + maxVisibleRows = self.maxVisibleRows + # The following block of code is executed by the lightweight proxy SearchBoxLight + """ # Call parent constructor super(SearchBoxLight, self).__init__(parent) # Connect signals and slots @@ -41,332 +44,382 @@ class SearchBox(QtGui.QLineEdit): self.setClearButtonEnabled(True) self.setPlaceholderText('Search tools, prefs & tree') self.setFixedWidth(200) # needed to avoid a change of width when the clear button appears/disappears - ''' + """ - # Save arguments - #self.model = model - self.getItemGroups = getItemGroups - self.getToolTip = getToolTip - self.itemGroups = None # Will be initialized by calling getItemGroups() the first time the search box gains focus, through focusInEvent and refreshItemGroups - self.maxVisibleRows = maxVisibleRows # TODO: use this to compute the correct height - # Create proxy model - self.proxyModel = QtCore.QIdentityProxyModel() - # Filtered model to which items are manually added. Store it as a property of the object instead of a local variable, to prevent grbage collection. - self.mdl = QtGui.QStandardItemModel() - #self.proxyModel.setModel(self.model) - # Create list view - self.listView = QtGui.QListView(self) - self.listView.setWindowFlags(QtGui.Qt.ToolTip) - self.listView.setWindowFlag(QtGui.Qt.FramelessWindowHint) - self.listView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) - self.listView.setModel(self.proxyModel) - self.listView.setItemDelegate(getItemDelegate()) # https://stackoverflow.com/a/65930408/324969 - # make the QListView non-editable - self.listView.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) - # Create pane for showing extra info about the currently-selected tool - #self.extraInfo = QtGui.QLabel() - self.extraInfo = QtGui.QWidget() - self.extraInfo.setWindowFlags(QtGui.Qt.ToolTip) - self.extraInfo.setWindowFlag(QtGui.Qt.FramelessWindowHint) - self.extraInfo.setLayout(QtGui.QVBoxLayout()) - self.extraInfo.layout().setContentsMargins(0,0,0,0) - self.setExtraInfoIsActive = False - self.pendingExtraInfo = None - self.currentExtraInfo = None - # Connect signals and slots - self.listView.clicked.connect(lambda x: self.selectResult('select', x)) - self.listView.selectionModel().selectionChanged.connect(self.onSelectionChanged) + # Save arguments + # self.model = model + self.getItemGroups = getItemGroups + self.getToolTip = getToolTip + self.itemGroups = None # Will be initialized by calling getItemGroups() the first time the search box gains focus, through focusInEvent and refreshItemGroups + self.maxVisibleRows = maxVisibleRows # TODO: use this to compute the correct height + # Create proxy model + self.proxyModel = QtCore.QIdentityProxyModel() + # Filtered model to which items are manually added. Store it as a property of the object instead of a local variable, to prevent grbage collection. + self.mdl = QtGui.QStandardItemModel() + # self.proxyModel.setModel(self.model) + # Create list view + self.listView = QtGui.QListView(self) + self.listView.setWindowFlags(QtGui.Qt.ToolTip) + self.listView.setWindowFlag(QtGui.Qt.FramelessWindowHint) + self.listView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) + self.listView.setModel(self.proxyModel) + self.listView.setItemDelegate(getItemDelegate()) # https://stackoverflow.com/a/65930408/324969 + # make the QListView non-editable + self.listView.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) + # Create pane for showing extra info about the currently-selected tool + # self.extraInfo = QtGui.QLabel() + self.extraInfo = QtGui.QWidget() + self.extraInfo.setWindowFlags(QtGui.Qt.ToolTip) + self.extraInfo.setWindowFlag(QtGui.Qt.FramelessWindowHint) + self.extraInfo.setLayout(QtGui.QVBoxLayout()) + self.extraInfo.layout().setContentsMargins(0, 0, 0, 0) + self.setExtraInfoIsActive = False + self.pendingExtraInfo = None + self.currentExtraInfo = None + # Connect signals and slots + self.listView.clicked.connect(lambda x: self.selectResult("select", x)) + self.listView.selectionModel().selectionChanged.connect(self.onSelectionChanged) - # Note: should probably use the eventFilter method instead... - wdgctx = QtCore.Qt.ShortcutContext.WidgetShortcut + # Note: should probably use the eventFilter method instead... + wdgctx = QtCore.Qt.ShortcutContext.WidgetShortcut - QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Down), self, context = wdgctx).activated.connect(self.listDown) - QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Up), self, context = wdgctx).activated.connect(self.listUp) - QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_PageDown), self, context = wdgctx).activated.connect(self.listPageDown) - QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_PageUp), self, context = wdgctx).activated.connect(self.listPageUp) + QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Down), self, context=wdgctx).activated.connect(self.listDown) + QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Up), self, context=wdgctx).activated.connect(self.listUp) + QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_PageDown), self, context=wdgctx).activated.connect( + self.listPageDown + ) + QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_PageUp), self, context=wdgctx).activated.connect( + self.listPageUp + ) - # Home and End do not work, for some reason. - #QtGui.QShortcut(QtGui.QKeySequence.MoveToEndOfDocument, self, context = wdgctx).activated.connect(self.listEnd) - #QtGui.QShortcut(QtGui.QKeySequence.MoveToStartOfDocument, self, context = wdgctx).activated.connect(self.listStart) - #QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_End), self, context = wdgctx).activated.connect(self.listEnd) - #QtGui.QShortcut(QtGui.QKeySequence('Home'), self, context = wdgctx).activated.connect(self.listStart) - - QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Enter), self, context = wdgctx).activated.connect(self.listAccept) - QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Return), self, context = wdgctx).activated.connect(self.listAccept) - QtGui.QShortcut(QtGui.QKeySequence('Ctrl+Return'), self, context = wdgctx).activated.connect(self.listAcceptToggle) - QtGui.QShortcut(QtGui.QKeySequence('Ctrl+Enter'), self, context = wdgctx).activated.connect(self.listAcceptToggle) - QtGui.QShortcut(QtGui.QKeySequence('Ctrl+Space'), self, context = wdgctx).activated.connect(self.listAcceptToggle) - - QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Escape), self, context = wdgctx).activated.connect(self.listCancel) + # Home and End do not work, for some reason. + # QtGui.QShortcut(QtGui.QKeySequence.MoveToEndOfDocument, self, context = wdgctx).activated.connect(self.listEnd) + # QtGui.QShortcut(QtGui.QKeySequence.MoveToStartOfDocument, self, context = wdgctx).activated.connect(self.listStart) + # QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_End), self, context = wdgctx).activated.connect(self.listEnd) + # QtGui.QShortcut(QtGui.QKeySequence('Home'), self, context = wdgctx).activated.connect(self.listStart) - # Initialize the model with the full list (assuming the text() is empty) - #self.proxyFilterModel(self.text()) # This is done by refreshItemGroups on focusInEvent, because the initial loading from cache can take time - self.firstShowList = True - self.isInitialized = True - return self + QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Enter), self, context=wdgctx).activated.connect( + self.listAccept + ) + QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Return), self, context=wdgctx).activated.connect( + self.listAccept + ) + QtGui.QShortcut(QtGui.QKeySequence("Ctrl+Return"), self, context=wdgctx).activated.connect( + self.listAcceptToggle + ) + QtGui.QShortcut(QtGui.QKeySequence("Ctrl+Enter"), self, context=wdgctx).activated.connect(self.listAcceptToggle) + QtGui.QShortcut(QtGui.QKeySequence("Ctrl+Space"), self, context=wdgctx).activated.connect(self.listAcceptToggle) - @staticmethod - def refreshItemGroups(self): - self.itemGroups = self.getItemGroups() - self.proxyFilterModel(self.text()) + QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Escape), self, context=wdgctx).activated.connect( + self.listCancel + ) - @staticmethod - def proxyFocusInEvent(self, qFocusEvent): - if self.firstShowList: - mdl = QtGui.QStandardItemModel() - mdl.appendRow([QtGui.QStandardItem(genericToolIcon, 'Please wait, loading results from cache…'), - QtGui.QStandardItem('0'), - QtGui.QStandardItem('-1')]) - self.proxyModel.setSourceModel(mdl) - self.showList() - self.firstShowList = False - FreeCADGui.updateGui() - global globalIgnoreFocusOut - if not globalIgnoreFocusOut: - self.refreshItemGroups() - self.showList() - super(SearchBoxLight, self).focusInEvent(qFocusEvent) + # Initialize the model with the full list (assuming the text() is empty) + # self.proxyFilterModel(self.text()) # This is done by refreshItemGroups on focusInEvent, because the initial loading from cache can take time + self.firstShowList = True + self.isInitialized = True + return self - @staticmethod - def proxyFocusOutEvent(self, qFocusEvent): - global globalIgnoreFocusOut - if not globalIgnoreFocusOut: - self.hideList() - super(SearchBoxLight, self).focusOutEvent(qFocusEvent) + @staticmethod + def refreshItemGroups(self): + self.itemGroups = self.getItemGroups() + self.proxyFilterModel(self.text()) - @staticmethod - def movementKey(self, rowUpdate): - currentIndex = self.listView.currentIndex() - self.showList() - if self.listView.isEnabled(): - currentRow = currentIndex.row() + @staticmethod + def proxyFocusInEvent(self, qFocusEvent): + if self.firstShowList: + mdl = QtGui.QStandardItemModel() + mdl.appendRow( + [ + QtGui.QStandardItem(genericToolIcon, "Please wait, loading results from cache…"), + QtGui.QStandardItem("0"), + QtGui.QStandardItem("-1"), + ] + ) + self.proxyModel.setSourceModel(mdl) + self.showList() + self.firstShowList = False + FreeCADGui.updateGui() + global globalIgnoreFocusOut + if not globalIgnoreFocusOut: + self.refreshItemGroups() + self.showList() + super(SearchBoxLight, self).focusInEvent(qFocusEvent) + + @staticmethod + def proxyFocusOutEvent(self, qFocusEvent): + global globalIgnoreFocusOut + if not globalIgnoreFocusOut: + self.hideList() + super(SearchBoxLight, self).focusOutEvent(qFocusEvent) + + @staticmethod + def movementKey(self, rowUpdate): + currentIndex = self.listView.currentIndex() + self.showList() + if self.listView.isEnabled(): + currentRow = currentIndex.row() + nbRows = self.listView.model().rowCount() + if nbRows > 0: + newRow = rowUpdate(currentRow, nbRows) + index = self.listView.model().index(newRow, 0) + self.listView.setCurrentIndex(index) + + @staticmethod + def proxyListDown(self): + self.movementKey(lambda current, nbRows: (current + 1) % nbRows) + + @staticmethod + def proxyListUp(self): + self.movementKey(lambda current, nbRows: (current - 1) % nbRows) + + @staticmethod + def proxyListPageDown(self): + self.movementKey(lambda current, nbRows: min(current + max(1, self.maxVisibleRows / 2), nbRows - 1)) + + @staticmethod + def proxyListPageUp(self): + self.movementKey(lambda current, nbRows: max(current - max(1, self.maxVisibleRows / 2), 0)) + + @staticmethod + def proxyListEnd(self): + self.movementKey(lambda current, nbRows: nbRows - 1) + + @staticmethod + def proxyListStart(self): + self.movementKey(lambda current, nbRows: 0) + + @staticmethod + def acceptKey(self, mode): + currentIndex = self.listView.currentIndex() + self.showList() + if currentIndex.isValid(): + self.selectResult(mode, currentIndex) + + @staticmethod + def proxyListAccept(self): + self.acceptKey("select") + + @staticmethod + def proxyListAcceptToggle(self): + self.acceptKey("toggle") + + @staticmethod + def cancelKey(self): + self.hideList() + self.clearFocus() + + # QKeySequence::Cancel + @staticmethod + def proxyListCancel(self): + self.cancelKey() + + @staticmethod + def proxyKeyPressEvent(self, qKeyEvent): + key = qKeyEvent.key() + modifiers = qKeyEvent.modifiers() + self.showList() + if key == QtCore.Qt.Key_Home and modifiers & QtCore.Qt.CTRL != 0: + self.listStart() + elif key == QtCore.Qt.Key_End and modifiers & QtCore.Qt.CTRL != 0: + self.listEnd() + else: + super(SearchBoxLight, self).keyPressEvent(qKeyEvent) + + @staticmethod + def showList(self): + self.setFloatingWidgetsGeometry() + if not self.listView.isVisible(): + self.listView.show() + self.showExtraInfo() + + @staticmethod + def hideList(self): + self.listView.hide() + self.hideExtraInfo() + + @staticmethod + def hideExtraInfo(self): + self.extraInfo.hide() + + @staticmethod + def selectResult(self, mode, index): + groupId = int(index.model().itemData(index.siblingAtColumn(2))[0]) + self.hideList() + # TODO: allow other options, e.g. some items could act as combinators / cumulative filters + self.setText("") + self.proxyFilterModel(self.text()) + # TODO: emit index relative to the base model + self.resultSelected.emit(index, groupId) + + @staticmethod + def proxyFilterModel(self, userInput): + # TODO: this will cause a race condition if it is accessed while being modified + def matches(s): + return userInput.lower() in s.lower() + + def filterGroup(group): + if matches(group["text"]): + # If a group matches, include the entire subtree (might need to disable this if it causes too much noise) + return group + else: + subitems = filterGroups(group["subitems"]) + if len(subitems) > 0 or matches(group["text"]): + return { + "id": group["id"], + "text": group["text"], + "icon": group["icon"], + "action": group["action"], + "toolTip": group["toolTip"], + "subitems": subitems, + } + else: + return None + + def filterGroups(groups): + groups = (filterGroup(group) for group in groups) + return [group for group in groups if group is not None] + + self.mdl = QtGui.QStandardItemModel() + self.mdl.appendColumn([]) + + def addGroups(filteredGroups, depth=0): + for group in filteredGroups: + # TODO: this is not very clean, we should memorize the index from the original itemgroups + self.mdl.appendRow( + [ + QtGui.QStandardItem(group["icon"] or genericToolIcon, group["text"]), + QtGui.QStandardItem(str(depth)), + QtGui.QStandardItem(str(group["id"])), + ] + ) + addGroups(group["subitems"], depth + 1) + + addGroups(filterGroups(self.itemGroups)) + self.proxyModel.setSourceModel(self.mdl) + self.currentExtraInfo = None # Unset this so that the ExtraInfo can be updated + # TODO: try to find the already-highlighted item nbRows = self.listView.model().rowCount() if nbRows > 0: - newRow = rowUpdate(currentRow, nbRows) - index = self.listView.model().index(newRow, 0) - self.listView.setCurrentIndex(index) - - @staticmethod - def proxyListDown(self): self.movementKey(lambda current, nbRows: (current + 1) % nbRows) - @staticmethod - def proxyListUp(self): self.movementKey(lambda current, nbRows: (current - 1) % nbRows) - @staticmethod - def proxyListPageDown(self): self.movementKey(lambda current, nbRows: min(current + max(1, self.maxVisibleRows / 2), nbRows - 1)) - @staticmethod - def proxyListPageUp(self): self.movementKey(lambda current, nbRows: max(current - max(1, self.maxVisibleRows / 2), 0)) - @staticmethod - def proxyListEnd(self): self.movementKey(lambda current, nbRows: nbRows - 1) - @staticmethod - def proxyListStart(self): self.movementKey(lambda current, nbRows: 0) - - @staticmethod - def acceptKey(self, mode): - currentIndex = self.listView.currentIndex() - self.showList() - if currentIndex.isValid(): - self.selectResult(mode, currentIndex) - - @staticmethod - def proxyListAccept(self): self.acceptKey('select') - @staticmethod - def proxyListAcceptToggle(self): self.acceptKey('toggle') - - @staticmethod - def cancelKey(self): - self.hideList() - self.clearFocus() - - # QKeySequence::Cancel - @staticmethod - def proxyListCancel(self): self.cancelKey() - - @staticmethod - def proxyKeyPressEvent(self, qKeyEvent): - key = qKeyEvent.key() - modifiers = qKeyEvent.modifiers() - self.showList() - if key == QtCore.Qt.Key_Home and modifiers & QtCore.Qt.CTRL != 0: - self.listStart() - elif key == QtCore.Qt.Key_End and modifiers & QtCore.Qt.CTRL != 0: - self.listEnd() - else: - super(SearchBoxLight, self).keyPressEvent(qKeyEvent) - - @staticmethod - def showList(self): - self.setFloatingWidgetsGeometry() - if not self.listView.isVisible(): - self.listView.show() - self.showExtraInfo() - - @staticmethod - def hideList(self): - self.listView.hide() - self.hideExtraInfo() - - @staticmethod - def hideExtraInfo(self): - self.extraInfo.hide() - - @staticmethod - def selectResult(self, mode, index): - groupId = int(index.model().itemData(index.siblingAtColumn(2))[0]) - self.hideList() - # TODO: allow other options, e.g. some items could act as combinators / cumulative filters - self.setText('') - self.proxyFilterModel(self.text()) - # TODO: emit index relative to the base model - self.resultSelected.emit(index, groupId) - - @staticmethod - def proxyFilterModel(self, userInput): - # TODO: this will cause a race condition if it is accessed while being modified - def matches(s): - return userInput.lower() in s.lower() - def filterGroup(group): - if matches(group['text']): - # If a group matches, include the entire subtree (might need to disable this if it causes too much noise) - return group - else: - subitems = filterGroups(group['subitems']) - if len(subitems) > 0 or matches(group['text']): - return { 'id': group['id'], 'text': group['text'], 'icon': group['icon'], 'action': group['action'], 'toolTip':group['toolTip'], 'subitems': subitems } + index = self.listView.model().index(0, 0) + self.listView.setCurrentIndex(index) + self.setExtraInfo(index) else: - return None - def filterGroups(groups): - groups = (filterGroup(group) for group in groups) - return [group for group in groups if group is not None] - self.mdl = QtGui.QStandardItemModel() - self.mdl.appendColumn([]) - def addGroups(filteredGroups, depth=0): - for group in filteredGroups: - # TODO: this is not very clean, we should memorize the index from the original itemgroups - self.mdl.appendRow([QtGui.QStandardItem(group['icon'] or genericToolIcon, group['text']), - QtGui.QStandardItem(str(depth)), - QtGui.QStandardItem(str(group['id']))]) - addGroups(group['subitems'], depth+1) - addGroups(filterGroups(self.itemGroups)) - self.proxyModel.setSourceModel(self.mdl) - self.currentExtraInfo = None # Unset this so that the ExtraInfo can be updated - # TODO: try to find the already-highlighted item - nbRows = self.listView.model().rowCount() - if nbRows > 0: - index = self.listView.model().index(0, 0) - self.listView.setCurrentIndex(index) - self.setExtraInfo(index) - else: - self.clearExtraInfo() - #self.showList() + self.clearExtraInfo() + # self.showList() - @staticmethod - def setFloatingWidgetsGeometry(self): - def getScreenPosition(widget): - geo = widget.geometry() - parent = widget.parent() - parentPos = getScreenPosition(parent) if parent is not None else QtCore.QPoint(0,0) - return QtCore.QPoint(geo.x() + parentPos.x(), geo.y() + parentPos.y()) - pos = getScreenPosition(self) - siz = self.size() - screen = QtGui.QGuiApplication.screenAt(pos) - x = pos.x() - y = pos.y() + siz.height() - hint_w = self.listView.sizeHint().width() - # TODO: this can still bump into the bottom of the screen, in that case we should flip - w = max(siz.width(), hint_w) - h = 200 # TODO: set height / size here according to desired number of items - extraw = w # choose a preferred width that doesn't change all the time, - # self.extraInfo.sizeHint().width() would change for every item. - extrax = x - extraw - if screen is not None: - scr = screen.geometry() - x = min(scr.x() + scr.width() - hint_w, x) - extraleftw = x - scr.x() - extrarightw = scr.x() + scr.width() - x - # flip the extraInfo if it doesn't fit on the screen - if extraleftw < extraw and extrarightw > extraleftw: - extrax = x + w - extraw = min(extrarightw, extraw) - else: + @staticmethod + def setFloatingWidgetsGeometry(self): + def getScreenPosition(widget): + geo = widget.geometry() + parent = widget.parent() + parentPos = getScreenPosition(parent) if parent is not None else QtCore.QPoint(0, 0) + return QtCore.QPoint(geo.x() + parentPos.x(), geo.y() + parentPos.y()) + + pos = getScreenPosition(self) + siz = self.size() + screen = QtGui.QGuiApplication.screenAt(pos) + x = pos.x() + y = pos.y() + siz.height() + hint_w = self.listView.sizeHint().width() + # TODO: this can still bump into the bottom of the screen, in that case we should flip + w = max(siz.width(), hint_w) + h = 200 # TODO: set height / size here according to desired number of items + extraw = w # choose a preferred width that doesn't change all the time, + # self.extraInfo.sizeHint().width() would change for every item. extrax = x - extraw - extraw = min(extraleftw, extraw) - self.listView.setGeometry(x, y, w, h) - self.extraInfo.setGeometry(extrax, y, extraw, h) + if screen is not None: + scr = screen.geometry() + x = min(scr.x() + scr.width() - hint_w, x) + extraleftw = x - scr.x() + extrarightw = scr.x() + scr.width() - x + # flip the extraInfo if it doesn't fit on the screen + if extraleftw < extraw and extrarightw > extraleftw: + extrax = x + w + extraw = min(extrarightw, extraw) + else: + extrax = x - extraw + extraw = min(extraleftw, extraw) + self.listView.setGeometry(x, y, w, h) + self.extraInfo.setGeometry(extrax, y, extraw, h) - @staticmethod - def proxyOnSelectionChanged(self, selected, deselected): - # The list has .setSelectionMode(QtGui.QAbstractItemView.SingleSelection), - # so there is always at most one index in selected.indexes() and at most one - # index in deselected.indexes() - selected = selected.indexes() - deselected = deselected.indexes() - if len(selected) > 0: - index = selected[0] - self.setExtraInfo(index) - # Poor attempt to circumvent a glitch where the extra info pane stays visible after pressing Return - if not self.listView.isHidden(): - self.showExtraInfo() - elif len(deselected) > 0: - self.hideExtraInfo() + @staticmethod + def proxyOnSelectionChanged(self, selected, deselected): + # The list has .setSelectionMode(QtGui.QAbstractItemView.SingleSelection), + # so there is always at most one index in selected.indexes() and at most one + # index in deselected.indexes() + selected = selected.indexes() + deselected = deselected.indexes() + if len(selected) > 0: + index = selected[0] + self.setExtraInfo(index) + # Poor attempt to circumvent a glitch where the extra info pane stays visible after pressing Return + if not self.listView.isHidden(): + self.showExtraInfo() + elif len(deselected) > 0: + self.hideExtraInfo() - @staticmethod - def setExtraInfo(self, index): - if self.currentExtraInfo == (index.row(), index.column(), index.model()): - # avoid useless updates of the extra info window; this also prevents segfaults when the widget - # is replaced when selecting an option from the right-click context menu - return - self.currentExtraInfo = (index.row(), index.column(), index.model()) - # TODO: use an atomic swap or mutex if possible - if self.setExtraInfoIsActive: - self.pendingExtraInfo = index - #print("boom") - else: - self.setExtraInfoIsActive = True - #print("lock") - # setExtraInfo can be called multiple times while this function is running, - # so just before existing we check for the latest pending call and execute it, - # if during that second execution some other calls are made the latest of those will - # be queued by the code a few lines above this one, and the loop will continue processing - # until an iteration during which no further call was made. - while True: - groupId = str(index.model().itemData(index.siblingAtColumn(2))[0]) - # TODO: move this outside of this class, probably use a single metadata - # This is a hack to allow some widgets to set the parent and recompute their size - # during their construction. - parentIsSet = False - def setParent(toolTipWidget): - nonlocal parentIsSet - parentIsSet = True - w = self.extraInfo.layout().takeAt(0) - while w: - if hasattr(w.widget(), 'finalizer'): - # The 3D viewer segfaults very easily if it is used after being destroyed, and some Python/C++ interop seems to overzealously destroys some widgets, including this one, too soon? - # Ensuring that we properly detacth the 3D viewer widget before discarding its parent seems to avoid these crashes. - #print('FINALIZER') - w.widget().finalizer() - if w.widget() is not None: - w.widget().hide() # hide before detaching, or we have widgets floating as their own window that appear for a split second in some cases. - w.widget().setParent(None) - w = self.extraInfo.layout().takeAt(0) - self.extraInfo.layout().addWidget(toolTipWidget) - self.setFloatingWidgetsGeometry() - toolTipWidget = self.getToolTip(groupId, setParent) - if isinstance(toolTipWidget, str): - toolTipWidget = easyToolTipWidget(toolTipWidget) - if not parentIsSet: - setParent(toolTipWidget) - if self.pendingExtraInfo is not None: - index = self.pendingExtraInfo - self.pendingExtraInfo = None + @staticmethod + def setExtraInfo(self, index): + if self.currentExtraInfo == (index.row(), index.column(), index.model()): + # avoid useless updates of the extra info window; this also prevents segfaults when the widget + # is replaced when selecting an option from the right-click context menu + return + self.currentExtraInfo = (index.row(), index.column(), index.model()) + # TODO: use an atomic swap or mutex if possible + if self.setExtraInfoIsActive: + self.pendingExtraInfo = index + # print("boom") else: - break - #print("unlock") - self.setExtraInfoIsActive = False + self.setExtraInfoIsActive = True + # print("lock") + # setExtraInfo can be called multiple times while this function is running, + # so just before existing we check for the latest pending call and execute it, + # if during that second execution some other calls are made the latest of those will + # be queued by the code a few lines above this one, and the loop will continue processing + # until an iteration during which no further call was made. + while True: + groupId = str(index.model().itemData(index.siblingAtColumn(2))[0]) + # TODO: move this outside of this class, probably use a single metadata + # This is a hack to allow some widgets to set the parent and recompute their size + # during their construction. + parentIsSet = False - @staticmethod - def clearExtraInfo(self): - # TODO: just clear the contents but keep the widget visible. - self.extraInfo.hide() + def setParent(toolTipWidget): + nonlocal parentIsSet + parentIsSet = True + w = self.extraInfo.layout().takeAt(0) + while w: + if hasattr(w.widget(), "finalizer"): + # The 3D viewer segfaults very easily if it is used after being destroyed, and some Python/C++ interop seems to overzealously destroys some widgets, including this one, too soon? + # Ensuring that we properly detacth the 3D viewer widget before discarding its parent seems to avoid these crashes. + # print('FINALIZER') + w.widget().finalizer() + if w.widget() is not None: + w.widget().hide() # hide before detaching, or we have widgets floating as their own window that appear for a split second in some cases. + w.widget().setParent(None) + w = self.extraInfo.layout().takeAt(0) + self.extraInfo.layout().addWidget(toolTipWidget) + self.setFloatingWidgetsGeometry() - @staticmethod - def showExtraInfo(self): - self.extraInfo.show() + toolTipWidget = self.getToolTip(groupId, setParent) + if isinstance(toolTipWidget, str): + toolTipWidget = easyToolTipWidget(toolTipWidget) + if not parentIsSet: + setParent(toolTipWidget) + if self.pendingExtraInfo is not None: + index = self.pendingExtraInfo + self.pendingExtraInfo = None + else: + break + # print("unlock") + self.setExtraInfoIsActive = False + + @staticmethod + def clearExtraInfo(self): + # TODO: just clear the contents but keep the widget visible. + self.extraInfo.hide() + + @staticmethod + def showExtraInfo(self): + self.extraInfo.show() diff --git a/SearchBoxLight.py b/SearchBoxLight.py index a11b160..ed129cb 100644 --- a/SearchBoxLight.py +++ b/SearchBoxLight.py @@ -1,49 +1,84 @@ -from PySide import QtGui -from PySide import QtCore +from PySide6 import QtGui +from PySide6 import QtCore + # This is a "light" version of the SearchBox implementation, which loads the actual implementation on first click class SearchBoxLight(QtGui.QLineEdit): - resultSelected = QtCore.Signal(int, int) - def __init__(self, getItemGroups, getToolTip, getItemDelegate, maxVisibleRows = 20, parent = None): - self.isInitialized = False + resultSelected = QtCore.Signal(int, int) - # Store arguments - self.getItemGroups = getItemGroups - self.getToolTip = getToolTip - self.getItemDelegate = getItemDelegate - self.maxVisibleRows = maxVisibleRows + def __init__(self, getItemGroups, getToolTip, getItemDelegate, maxVisibleRows=20, parent=None): + self.isInitialized = False - # Call parent constructor - super(SearchBoxLight, self).__init__(parent) - # Connect signals and slots - self.textChanged.connect(self.filterModel) - # Thanks to https://saurabhg.com/programming/search-box-using-qlineedit/ for indicating a few useful options - ico = QtGui.QIcon(':/icons/help-browser.svg') - #ico = QtGui.QIcon(':/icons/WhatsThis.svg') - self.addAction(ico, QtGui.QLineEdit.LeadingPosition) - self.setClearButtonEnabled(True) - self.setPlaceholderText('Search tools, prefs & tree') - self.setFixedWidth(200) # needed to avoid a change of width when the clear button appears/disappears - def lazyInit(self): - pass - def __getattr__(self, name): - import types - def f(*args, **kwargs): - import SearchBox - SearchBox.SearchBox.lazyInit(self) - return getattr(SearchBox.SearchBox, name)(*args, **kwargs) - return types.MethodType(f, self) - def focusInEvent(self, *args, **kwargs): return self.proxyFocusInEvent(*args, **kwargs) - def focusOutEvent(self, *args, **kwargs): return self.proxyFocusOutEvent(*args, **kwargs) - def keyPressEvent(self, *args, **kwargs): return self.proxyKeyPressEvent(*args, **kwargs) - def onSelectionChanged(self, *args, **kwargs): return self.proxyOnSelectionChanged(*args, **kwargs) - def filterModel(self, *args, **kwargs): return self.proxyFilterModel(*args, **kwargs) - def listDown(self, *args, **kwargs): return self.proxyListDown(*args, **kwargs) - def listUp(self, *args, **kwargs): return self.proxyListUp(*args, **kwargs) - def listPageDown(self, *args, **kwargs): return self.proxyListPageDown(*args, **kwargs) - def listPageUp(self, *args, **kwargs): return self.proxyListPageUp(*args, **kwargs) - def listEnd(self, *args, **kwargs): return self.proxyListEnd(*args, **kwargs) - def listStart(self, *args, **kwargs): return self.proxyListStart(*args, **kwargs) - def listAccept(self, *args, **kwargs): return self.proxyListAccept(*args, **kwargs) - def listAcceptToggle(self, *args, **kwargs): return self.proxyListAcceptToggle(*args, **kwargs) - def listCancel(self, *args, **kwargs): return self.proxyListCancel(*args, **kwargs) + # Store arguments + self.getItemGroups = getItemGroups + self.getToolTip = getToolTip + self.getItemDelegate = getItemDelegate + self.maxVisibleRows = maxVisibleRows + + # Call parent constructor + super(SearchBoxLight, self).__init__(parent) + # Connect signals and slots + self.textChanged.connect(self.filterModel) + # Thanks to https://saurabhg.com/programming/search-box-using-qlineedit/ for indicating a few useful options + ico = QtGui.QIcon(":/icons/help-browser.svg") + # ico = QtGui.QIcon(':/icons/WhatsThis.svg') + self.addAction(ico, QtGui.QLineEdit.LeadingPosition) + self.setClearButtonEnabled(True) + self.setPlaceholderText("Search tools, prefs & tree") + self.setFixedWidth(200) # needed to avoid a change of width when the clear button appears/disappears + + def lazyInit(self): + pass + + def __getattr__(self, name): + import types + + def f(*args, **kwargs): + import SearchBox + + SearchBox.SearchBox.lazyInit(self) + return getattr(SearchBox.SearchBox, name)(*args, **kwargs) + + return types.MethodType(f, self) + + def focusInEvent(self, *args, **kwargs): + return self.proxyFocusInEvent(*args, **kwargs) + + def focusOutEvent(self, *args, **kwargs): + return self.proxyFocusOutEvent(*args, **kwargs) + + def keyPressEvent(self, *args, **kwargs): + return self.proxyKeyPressEvent(*args, **kwargs) + + def onSelectionChanged(self, *args, **kwargs): + return self.proxyOnSelectionChanged(*args, **kwargs) + + def filterModel(self, *args, **kwargs): + return self.proxyFilterModel(*args, **kwargs) + + def listDown(self, *args, **kwargs): + return self.proxyListDown(*args, **kwargs) + + def listUp(self, *args, **kwargs): + return self.proxyListUp(*args, **kwargs) + + def listPageDown(self, *args, **kwargs): + return self.proxyListPageDown(*args, **kwargs) + + def listPageUp(self, *args, **kwargs): + return self.proxyListPageUp(*args, **kwargs) + + def listEnd(self, *args, **kwargs): + return self.proxyListEnd(*args, **kwargs) + + def listStart(self, *args, **kwargs): + return self.proxyListStart(*args, **kwargs) + + def listAccept(self, *args, **kwargs): + return self.proxyListAccept(*args, **kwargs) + + def listAcceptToggle(self, *args, **kwargs): + return self.proxyListAcceptToggle(*args, **kwargs) + + def listCancel(self, *args, **kwargs): + return self.proxyListCancel(*args, **kwargs) diff --git a/Serialize.py b/Serialize.py index 3b891ba..a163e30 100644 --- a/Serialize.py +++ b/Serialize.py @@ -1,87 +1,130 @@ -from PySide import QtCore -from PySide import QtGui +from PySide6 import QtCore +from PySide6 import QtGui import json -def iconToBase64(icon, sz = QtCore.QSize(64,64), mode = QtGui.QIcon.Mode.Normal, state = QtGui.QIcon.State.On): - buf = QtCore.QBuffer() - buf.open(QtCore.QIODevice.WriteOnly) - icon.pixmap(sz, mode, state).save(buf, 'PNG') - return QtCore.QTextCodec.codecForName('UTF-8').toUnicode(buf.data().toBase64()) -def iconToHTML(icon, sz = 12, mode = QtGui.QIcon.Mode.Normal, state = QtGui.QIcon.State.On): - return '' +def iconToBase64(icon, sz=QtCore.QSize(64, 64), mode=QtGui.QIcon.Mode.Normal, state=QtGui.QIcon.State.On): + buf = QtCore.QBuffer() + buf.open(QtCore.QIODevice.WriteOnly) + icon.pixmap(sz, mode, state).save(buf, "PNG") + + result = None + try: + result = QtCore.QTextCodec.codecForName("UTF-8").toUnicode(buf.data().toBase64()) + except Exception: + t = QtCore.QTextStream(buf.data().toBase64()) + t.setEncoding(QtCore.QStringDecoder.Encoding.Utf8) + result = t.readAll() + return result + + +def iconToHTML(icon, sz=12, mode=QtGui.QIcon.Mode.Normal, state=QtGui.QIcon.State.On): + return ( + '' + ) + def serializeIcon(icon): - iconPixmaps = {} - for sz in icon.availableSizes(): - strW = str(sz.width()) - strH = str(sz.height()) - iconPixmaps[strW] = {} - iconPixmaps[strW][strH] = {} - for strMode, mode in {'normal':QtGui.QIcon.Mode.Normal, 'disabled':QtGui.QIcon.Mode.Disabled, 'active':QtGui.QIcon.Mode.Active, 'selected':QtGui.QIcon.Mode.Selected}.items(): - iconPixmaps[strW][strH][strMode] = {} - for strState, state in {'off':QtGui.QIcon.State.Off, 'on':QtGui.QIcon.State.On}.items(): - iconPixmaps[strW][strH][strMode][strState] = iconToBase64(icon, sz, mode, state) - return iconPixmaps + iconPixmaps = {} + for sz in icon.availableSizes(): + strW = str(sz.width()) + strH = str(sz.height()) + iconPixmaps[strW] = {} + iconPixmaps[strW][strH] = {} + for strMode, mode in { + "normal": QtGui.QIcon.Mode.Normal, + "disabled": QtGui.QIcon.Mode.Disabled, + "active": QtGui.QIcon.Mode.Active, + "selected": QtGui.QIcon.Mode.Selected, + }.items(): + iconPixmaps[strW][strH][strMode] = {} + for strState, state in {"off": QtGui.QIcon.State.Off, "on": QtGui.QIcon.State.On}.items(): + iconPixmaps[strW][strH][strMode][strState] = iconToBase64(icon, sz, mode, state) + return iconPixmaps + # workbenches is a list(str), toolbar is a str, text is a str, icon is a QtGui.QIcon def serializeTool(tool): - return { - 'workbenches': tool['workbenches'], - 'toolbar': tool['toolbar'], - 'text': tool['text'], - 'toolTip': tool['toolTip'], - 'icon': serializeIcon(tool['icon']), - } + return { + "workbenches": tool["workbenches"], + "toolbar": tool["toolbar"], + "text": tool["text"], + "toolTip": tool["toolTip"], + "icon": serializeIcon(tool["icon"]), + } + def deserializeIcon(iconPixmaps): - ico = QtGui.QIcon() - for strW, wPixmaps in iconPixmaps.items(): - for strH, hPixmaps in wPixmaps.items(): - for strMode, modePixmaps in hPixmaps.items(): - mode = {'normal':QtGui.QIcon.Mode.Normal, 'disabled':QtGui.QIcon.Mode.Disabled, 'active':QtGui.QIcon.Mode.Active, 'selected':QtGui.QIcon.Mode.Selected}[strMode] - for strState, statePixmap in modePixmaps.items(): - state = {'off':QtGui.QIcon.State.Off, 'on':QtGui.QIcon.State.On}[strState] - pxm = QtGui.QPixmap() - pxm.loadFromData(QtCore.QByteArray.fromBase64(QtCore.QTextCodec.codecForName('UTF-8').fromUnicode(statePixmap))) - ico.addPixmap(pxm, mode, state) - return ico + ico = QtGui.QIcon() + for strW, wPixmaps in iconPixmaps.items(): + for strH, hPixmaps in wPixmaps.items(): + for strMode, modePixmaps in hPixmaps.items(): + mode = { + "normal": QtGui.QIcon.Mode.Normal, + "disabled": QtGui.QIcon.Mode.Disabled, + "active": QtGui.QIcon.Mode.Active, + "selected": QtGui.QIcon.Mode.Selected, + }[strMode] + for strState, statePixmap in modePixmaps.items(): + state = {"off": QtGui.QIcon.State.Off, "on": QtGui.QIcon.State.On}[strState] + pxm = QtGui.QPixmap() + # pxm.loadFromData( + # QtCore.QByteArray.fromBase64(QtCore.QTextCodec.codecForName("UTF-8").fromUnicode(statePixmap)) + # ) + t = QtCore.QTextStream(statePixmap) + t.setEncoding(QtCore.QStringDecoder.Encoding.Utf8) + pxm.loadFromData(t.readAll()) + ico.addPixmap(pxm, mode, state) + return ico + def deserializeTool(tool): - return { - 'workbenches': tool['workbenches'], - 'toolbar': tool['toolbar'], - 'text': tool['text'], - 'toolTip': tool['toolTip'], - 'icon': deserializeIcon(tool['icon']), - } + return { + "workbenches": tool["workbenches"], + "toolbar": tool["toolbar"], + "text": tool["text"], + "toolTip": tool["toolTip"], + "icon": deserializeIcon(tool["icon"]), + } + def serializeItemGroup(itemGroup): - return { - 'icon': serializeIcon(itemGroup['icon']), - 'text': itemGroup['text'], - 'toolTip': itemGroup['toolTip'], - 'action': itemGroup['action'], - 'subitems': serializeItemGroups(itemGroup['subitems']) - } + return { + "icon": serializeIcon(itemGroup["icon"]), + "text": itemGroup["text"], + "toolTip": itemGroup["toolTip"], + "action": itemGroup["action"], + "subitems": serializeItemGroups(itemGroup["subitems"]), + } + def serializeItemGroups(itemGroups): - return [serializeItemGroup(itemGroup) for itemGroup in itemGroups] + return [serializeItemGroup(itemGroup) for itemGroup in itemGroups] + def serialize(itemGroups): - return json.dumps(serializeItemGroups(itemGroups)) + return json.dumps(serializeItemGroups(itemGroups)) + def deserializeItemGroup(itemGroup): - return { - 'icon': deserializeIcon(itemGroup['icon']), - 'text': itemGroup['text'], - 'toolTip': itemGroup['toolTip'], - 'action': itemGroup['action'], - 'subitems': deserializeItemGroups(itemGroup['subitems']) - } + return { + "icon": deserializeIcon(itemGroup["icon"]), + "text": itemGroup["text"], + "toolTip": itemGroup["toolTip"], + "action": itemGroup["action"], + "subitems": deserializeItemGroups(itemGroup["subitems"]), + } + def deserializeItemGroups(serializedItemGroups): - return [deserializeItemGroup(itemGroup) for itemGroup in serializedItemGroups] + return [deserializeItemGroup(itemGroup) for itemGroup in serializedItemGroups] + def deserialize(serializedItemGroups): - return deserializeItemGroups(json.loads(serializedItemGroups)) + return deserializeItemGroups(json.loads(serializedItemGroups)) From 036f1919f2daaecc56cf0c339652c3e629581e80 Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Thu, 7 Nov 2024 23:21:01 +0100 Subject: [PATCH 02/47] Changed PySide6 to PySide --- GetItemGroups.py | 2 +- IndentedItemDelegate.py | 2 +- InitGui.py | 2 +- RefreshTools.py | 4 ++-- ResultsDocument.py | 4 ++-- ResultsPreferences.py | 2 +- ResultsRefreshTools.py | 2 +- ResultsToolbar.py | 2 +- SafeViewer.py | 6 +++--- SearchBox.py | 4 ++-- SearchBoxLight.py | 4 ++-- Serialize.py | 4 ++-- 12 files changed, 19 insertions(+), 19 deletions(-) diff --git a/GetItemGroups.py b/GetItemGroups.py index 56262b4..4fa75e1 100644 --- a/GetItemGroups.py +++ b/GetItemGroups.py @@ -13,7 +13,7 @@ def onResultSelected(index, groupId): if handlerName in SearchResults.actionHandlers: SearchResults.actionHandlers[handlerName](nfo) else: - from PySide6 import QtGui + from PySide import QtGui QtGui.QMessageBox.warning( None, diff --git a/IndentedItemDelegate.py b/IndentedItemDelegate.py index 76d2d63..b283a8e 100644 --- a/IndentedItemDelegate.py +++ b/IndentedItemDelegate.py @@ -1,4 +1,4 @@ -from PySide6 import QtGui +from PySide import QtGui # Inspired by https://stackoverflow.com/a/5443220/324969 diff --git a/InitGui.py b/InitGui.py index bd0a373..295678e 100644 --- a/InitGui.py +++ b/InitGui.py @@ -6,7 +6,7 @@ tbr = None def addToolSearchBox(): import FreeCADGui - from PySide6 import QtGui + from PySide import QtGui import SearchBoxLight global wax, sea, tbr diff --git a/RefreshTools.py b/RefreshTools.py index ad7c6f9..041c5d7 100644 --- a/RefreshTools.py +++ b/RefreshTools.py @@ -3,7 +3,7 @@ import FreeCAD as App def loadAllWorkbenches(): - from PySide6 import QtGui + from PySide import QtGui import FreeCADGui activeWorkbench = FreeCADGui.activeWorkbench().name() @@ -80,7 +80,7 @@ def refreshToolbars(doLoadAllWorkbenches=True): def refreshToolsAction(): - from PySide6 import QtGui + from PySide import QtGui print("Refresh list of tools") fw = QtGui.QApplication.focusWidget() diff --git a/ResultsDocument.py b/ResultsDocument.py index 44435b1..2e0f8a1 100644 --- a/ResultsDocument.py +++ b/ResultsDocument.py @@ -1,5 +1,5 @@ -from PySide6 import QtGui -from PySide6 import QtCore +from PySide import QtGui +from PySide import QtCore import FreeCAD as App import FreeCADGui import SafeViewer diff --git a/ResultsPreferences.py b/ResultsPreferences.py index 3ba67f4..f45d549 100644 --- a/ResultsPreferences.py +++ b/ResultsPreferences.py @@ -1,7 +1,7 @@ import os import FreeCAD as App import FreeCADGui -from PySide6 import QtGui +from PySide import QtGui import Serialize genericToolIcon = QtGui.QIcon(QtGui.QIcon(os.path.dirname(__file__) + "/Tango-Tools-spanner-hammer.svg")) diff --git a/ResultsRefreshTools.py b/ResultsRefreshTools.py index 07f7915..cfa8515 100644 --- a/ResultsRefreshTools.py +++ b/ResultsRefreshTools.py @@ -1,5 +1,5 @@ import os -from PySide6 import QtGui +from PySide import QtGui import Serialize diff --git a/ResultsToolbar.py b/ResultsToolbar.py index be61be6..4e38d79 100644 --- a/ResultsToolbar.py +++ b/ResultsToolbar.py @@ -1,4 +1,4 @@ -from PySide6 import QtGui +from PySide import QtGui import FreeCADGui import Serialize diff --git a/SafeViewer.py b/SafeViewer.py index 37973ef..2832b95 100644 --- a/SafeViewer.py +++ b/SafeViewer.py @@ -1,4 +1,4 @@ -from PySide6 import QtGui +from PySide import QtGui import FreeCAD @@ -20,7 +20,7 @@ class SafeViewer(QtGui.QWidget): self.enable() else: import FreeCADGui - from PySide6 import QtCore + from PySide import QtCore self.displaying_warning = True self.lbl_warning = QtGui.QTextEdit() @@ -114,7 +114,7 @@ class SafeViewer(QtGui.QWidget): """ # Example use: -from PySide6 import QtGui +from PySide import QtGui import pivy from SafeViewer import SafeViewer sv = SafeViewer() diff --git a/SearchBox.py b/SearchBox.py index 4ffe46c..78880d0 100644 --- a/SearchBox.py +++ b/SearchBox.py @@ -1,6 +1,6 @@ import os -from PySide6 import QtGui -from PySide6 import QtCore +from PySide import QtGui +from PySide import QtCore import FreeCADGui # just used for FreeCADGui.updateGui() from SearchBoxLight import SearchBoxLight diff --git a/SearchBoxLight.py b/SearchBoxLight.py index ed129cb..3801ded 100644 --- a/SearchBoxLight.py +++ b/SearchBoxLight.py @@ -1,5 +1,5 @@ -from PySide6 import QtGui -from PySide6 import QtCore +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 diff --git a/Serialize.py b/Serialize.py index a163e30..d51b867 100644 --- a/Serialize.py +++ b/Serialize.py @@ -1,5 +1,5 @@ -from PySide6 import QtCore -from PySide6 import QtGui +from PySide import QtCore +from PySide import QtGui import json From 23df9a8686f11b022bc9423507e471bdf1b8561b Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Thu, 7 Nov 2024 23:31:50 +0100 Subject: [PATCH 03/47] test2 --- Serialize.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Serialize.py b/Serialize.py index d51b867..92b213f 100644 --- a/Serialize.py +++ b/Serialize.py @@ -77,9 +77,12 @@ def deserializeIcon(iconPixmaps): # pxm.loadFromData( # QtCore.QByteArray.fromBase64(QtCore.QTextCodec.codecForName("UTF-8").fromUnicode(statePixmap)) # ) - t = QtCore.QTextStream(statePixmap) - t.setEncoding(QtCore.QStringDecoder.Encoding.Utf8) - pxm.loadFromData(t.readAll()) + # t = QtCore.QTextStream(bytearray(statePixmap)) + # t.setEncoding(QtCore.QStringDecoder.Encoding.Utf8) + # pxm.loadFromData(t.readAll()) + encoded = statePixmap.encode("utf-8") + array = bytearray(encoded) + pxm.loadFromData(array) ico.addPixmap(pxm, mode, state) return ico From 6c9c3f474d79ca97b4be4a231f0ff65b5acbd960 Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Fri, 8 Nov 2024 00:58:22 +0100 Subject: [PATCH 04/47] Fixed issue with PySide6 and Obsolete QTextCodec --- Serialize.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Serialize.py b/Serialize.py index 92b213f..cfdd6d0 100644 --- a/Serialize.py +++ b/Serialize.py @@ -74,15 +74,7 @@ def deserializeIcon(iconPixmaps): for strState, statePixmap in modePixmaps.items(): state = {"off": QtGui.QIcon.State.Off, "on": QtGui.QIcon.State.On}[strState] pxm = QtGui.QPixmap() - # pxm.loadFromData( - # QtCore.QByteArray.fromBase64(QtCore.QTextCodec.codecForName("UTF-8").fromUnicode(statePixmap)) - # ) - # t = QtCore.QTextStream(bytearray(statePixmap)) - # t.setEncoding(QtCore.QStringDecoder.Encoding.Utf8) - # pxm.loadFromData(t.readAll()) - encoded = statePixmap.encode("utf-8") - array = bytearray(encoded) - pxm.loadFromData(array) + pxm.loadFromData(QtCore.QByteArray.fromBase64(bytearray(statePixmap.encode("utf-8")))) ico.addPixmap(pxm, mode, state) return ico From 840f0c7b54349027ab021502b2202f61841f00ad Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Fri, 8 Nov 2024 19:08:09 +0100 Subject: [PATCH 05/47] Updated package.xml --- package.xml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/package.xml b/package.xml index 7511316..cd117ca 100644 --- a/package.xml +++ b/package.xml @@ -2,14 +2,13 @@ SearchBar Adds a search bar widget for tools, document objects, and preferences - 1.0.2 + 1.1.0 2022-06-01 - Suzanne Soy - Suzanne Soy + Paul Ebbers CCOv1 - https://github.com/SuzanneSoy/SearchBar - https://github.com/SuzanneSoy/SearchBar/issues - https://github.com/SuzanneSoy/SearchBar + https://github.com/APEbbers/SearchBar + https://github.com/APEbbers/SearchBar/issues + https://github.com/APEbbers/SearchBar Tango-System-search.svg lxml From 8b056948b0bd0287452926b4b184a6d29ce9c8cb Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Wed, 13 Nov 2024 18:40:01 +0100 Subject: [PATCH 06/47] Update package.xml --- package.xml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/package.xml b/package.xml index cd117ca..313a443 100644 --- a/package.xml +++ b/package.xml @@ -1,27 +1,34 @@ + SearchBar + Adds a search bar widget for tools, document objects, and preferences + 1.1.0 + 2022-06-01 + Paul Ebbers + CCOv1 + https://github.com/APEbbers/SearchBar + https://github.com/APEbbers/SearchBar/issues + https://github.com/APEbbers/SearchBar - Tango-System-search.svg + lxml SearchBar - Adds a search bar widget for tools, document objects, and preferences - Tango-System-search.svg ./ search widget ui/ux - 0.1.0 + From 5ae025cc6139de7bcfa62a48d0603d3b647a5974 Mon Sep 17 00:00:00 2001 From: luzpaz Date: Wed, 13 Nov 2024 15:49:10 -0500 Subject: [PATCH 07/47] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 064f6ac..e7747a8 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,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). From 159488b35a082c03509029fefbf2974a34253cb3 Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Fri, 29 Nov 2024 13:54:32 +0100 Subject: [PATCH 08/47] update settings.json --- .vscode/settings.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..febb024 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "githubPullRequests.ignoredPullRequestBranches": ["main"] +} From ce852b08170bdb3c9eb024e953096ae6f35e095f Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Fri, 29 Nov 2024 13:56:47 +0100 Subject: [PATCH 09/47] new serialize function --- Serialize.py | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/Serialize.py b/Serialize.py index cfdd6d0..e2c0847 100644 --- a/Serialize.py +++ b/Serialize.py @@ -3,19 +3,34 @@ 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") +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. - result = None + 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: - result = QtCore.QTextCodec.codecForName("UTF-8").toUnicode(buf.data().toBase64()) - except Exception: - t = QtCore.QTextStream(buf.data().toBase64()) - t.setEncoding(QtCore.QStringDecoder.Encoding.Utf8) - result = t.readAll() - return result + 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): From 8d244addd15eca1eb8b9d48bb55735ad4cd83069 Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Fri, 29 Nov 2024 14:03:10 +0100 Subject: [PATCH 10/47] update version --- package.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.xml b/package.xml index 313a443..6bc9f7d 100644 --- a/package.xml +++ b/package.xml @@ -5,7 +5,7 @@ Adds a search bar widget for tools, document objects, and preferences - 1.1.0 + 1.2.0 2022-06-01 From d11564e84ae6adb11852de5296d88699e68e8fb7 Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Sat, 30 Nov 2024 18:22:43 +0100 Subject: [PATCH 11/47] Changed Serialize.py to Serialize_SearcdhBar.py --- GetItemGroups.py | 4 ++-- RefreshTools.py | 12 ++++++------ ResultsPreferences.py | 2 +- ResultsRefreshTools.py | 4 ++-- ResultsToolbar.py | 9 ++++++--- Serialize.py => Serialize_SearchBar.py | 0 package.xml | 2 +- 7 files changed, 18 insertions(+), 15 deletions(-) rename Serialize.py => Serialize_SearchBar.py (100%) diff --git a/GetItemGroups.py b/GetItemGroups.py index 4fa75e1..6e51a27 100644 --- a/GetItemGroups.py +++ b/GetItemGroups.py @@ -48,9 +48,9 @@ def getItemGroups(): itemGroups = RefreshTools.refreshToolbars(doLoadAllWorkbenches=False) else: - import Serialize + import Serialize_SearchBar - itemGroups = Serialize.deserialize(serializedItemGroups) + itemGroups = Serialize_SearchBar.deserialize(serializedItemGroups) # Aggregate the tools (cached) and document objects (not cached), and assign an index to each import SearchResults diff --git a/RefreshTools.py b/RefreshTools.py index 041c5d7..b97887f 100644 --- a/RefreshTools.py +++ b/RefreshTools.py @@ -41,18 +41,18 @@ def gatherTools(): def writeCacheTools(): - import Serialize + import Serialize_SearchBar - serializedItemGroups = Serialize.serialize(gatherTools()) + serializedItemGroups = Serialize_SearchBar.serialize(gatherTools()) # Todo: use wb and a specific encoding. with open(cachePath(), "w") as cache: cache.write(serializedItemGroups) # I prefer to systematically deserialize, instead of taking the original version, # this avoids possible inconsistencies between the original and the cache and # makes sure cache-related bugs are noticed quickly. - import Serialize + import Serialize_SearchBar - itemGroups = Serialize.deserialize(serializedItemGroups) + itemGroups = Serialize_SearchBar.deserialize(serializedItemGroups) print("SearchBox: Cache has been written.") return itemGroups @@ -61,9 +61,9 @@ def readCacheTools(): # Todo: use rb and a specific encoding. with open(cachePath(), "r") as cache: serializedItemGroups = cache.read() - import Serialize + import Serialize_SearchBar - itemGroups = Serialize.deserialize(serializedItemGroups) + itemGroups = Serialize_SearchBar.deserialize(serializedItemGroups) print("SearchBox: Tools were loaded from the cache.") return itemGroups diff --git a/ResultsPreferences.py b/ResultsPreferences.py index f45d549..5d313a7 100644 --- a/ResultsPreferences.py +++ b/ResultsPreferences.py @@ -2,7 +2,7 @@ import os import FreeCAD as App import FreeCADGui from PySide import QtGui -import Serialize +import Serialize_SearchBar genericToolIcon = QtGui.QIcon(QtGui.QIcon(os.path.dirname(__file__) + "/Tango-Tools-spanner-hammer.svg")) diff --git a/ResultsRefreshTools.py b/ResultsRefreshTools.py index cfa8515..0f10396 100644 --- a/ResultsRefreshTools.py +++ b/ResultsRefreshTools.py @@ -1,6 +1,6 @@ import os from PySide import QtGui -import Serialize +import Serialize_SearchBar def refreshToolsAction(nfo): @@ -11,7 +11,7 @@ def refreshToolsAction(nfo): def refreshToolsToolTip(nfo, setParent): return ( - Serialize.iconToHTML(genericToolIcon) + Serialize_SearchBar.iconToHTML(genericToolIcon) + "

    Load all workbenches to refresh this list of tools. This may take a minute, depending on the number of installed workbenches.

    " ) diff --git a/ResultsToolbar.py b/ResultsToolbar.py index 4e38d79..ec41db2 100644 --- a/ResultsToolbar.py +++ b/ResultsToolbar.py @@ -1,6 +1,6 @@ from PySide import QtGui import FreeCADGui -import Serialize +import Serialize_SearchBar def toolbarAction(nfo): @@ -61,7 +61,10 @@ def subToolAction(nfo): def toolbarToolTip(nfo, setParent): workbenches = FreeCADGui.listWorkbenches() in_workbenches = [ - "
  • " + (Serialize.iconToHTML(QtGui.QIcon(workbenches[wb].Icon)) if wb in workbenches else "? ") + wb + "
  • " + "
  • " + + (Serialize_SearchBar.iconToHTML(QtGui.QIcon(workbenches[wb].Icon)) if wb in workbenches else "? ") + + wb + + "
  • " for wb in nfo["action"]["workbenches"] ] return ( @@ -74,7 +77,7 @@ def toolbarToolTip(nfo, setParent): def subToolToolTip(nfo, setParent): - return Serialize.iconToHTML(nfo["icon"], 32) + "

    " + nfo["toolTip"] + "

    " + return Serialize_SearchBar.iconToHTML(nfo["icon"], 32) + "

    " + nfo["toolTip"] + "

    " def getAllToolbars(): diff --git a/Serialize.py b/Serialize_SearchBar.py similarity index 100% rename from Serialize.py rename to Serialize_SearchBar.py diff --git a/package.xml b/package.xml index 6bc9f7d..ac7d3c0 100644 --- a/package.xml +++ b/package.xml @@ -5,7 +5,7 @@ Adds a search bar widget for tools, document objects, and preferences - 1.2.0 + 1.2.1 2022-06-01 From 53ed074ba5af70164ec424edc934b9b2711559df Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Fri, 10 Jan 2025 15:21:24 +0100 Subject: [PATCH 12/47] Added resources folder --- README.md | 6 +++--- .../Icons/Tango-System-search.svg | 2 +- .../Icons/Tango-System-search.svg.url | 0 .../Icons/Tango-Tools-spanner-hammer.svg | 0 .../Icons/Tango-Tools-spanner-hammer.svg.url | 0 animAopt.gif => Resources/Images/animAopt.gif | Bin animB2op.gif => Resources/Images/animB2op.gif | Bin screenshot.png => Resources/Images/screenshot.png | Bin ResultsPreferences.py | 5 ++++- package.xml | 4 ++-- 10 files changed, 10 insertions(+), 7 deletions(-) rename Tango-System-search.svg => Resources/Icons/Tango-System-search.svg (99%) rename Tango-System-search.svg.url => Resources/Icons/Tango-System-search.svg.url (100%) rename Tango-Tools-spanner-hammer.svg => Resources/Icons/Tango-Tools-spanner-hammer.svg (100%) rename Tango-Tools-spanner-hammer.svg.url => Resources/Icons/Tango-Tools-spanner-hammer.svg.url (100%) rename animAopt.gif => Resources/Images/animAopt.gif (100%) rename animB2op.gif => Resources/Images/animB2op.gif (100%) rename screenshot.png => Resources/Images/screenshot.png (100%) diff --git a/README.md b/README.md index e7747a8..9a7862c 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ 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 drawing 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 @@ -22,14 +22,14 @@ and memorize their tools. After restarting FreeCAD, the search result will inclu 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. -![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 diff --git a/Tango-System-search.svg b/Resources/Icons/Tango-System-search.svg similarity index 99% rename from Tango-System-search.svg rename to Resources/Icons/Tango-System-search.svg index dee0fdc..f98b2ea 100644 --- a/Tango-System-search.svg +++ b/Resources/Icons/Tango-System-search.svg @@ -42,5 +42,5 @@ - + \ No newline at end of file diff --git a/Tango-System-search.svg.url b/Resources/Icons/Tango-System-search.svg.url similarity index 100% rename from Tango-System-search.svg.url rename to Resources/Icons/Tango-System-search.svg.url diff --git a/Tango-Tools-spanner-hammer.svg b/Resources/Icons/Tango-Tools-spanner-hammer.svg similarity index 100% rename from Tango-Tools-spanner-hammer.svg rename to Resources/Icons/Tango-Tools-spanner-hammer.svg diff --git a/Tango-Tools-spanner-hammer.svg.url b/Resources/Icons/Tango-Tools-spanner-hammer.svg.url similarity index 100% rename from Tango-Tools-spanner-hammer.svg.url rename to Resources/Icons/Tango-Tools-spanner-hammer.svg.url diff --git a/animAopt.gif b/Resources/Images/animAopt.gif similarity index 100% rename from animAopt.gif rename to Resources/Images/animAopt.gif diff --git a/animB2op.gif b/Resources/Images/animB2op.gif similarity index 100% rename from animB2op.gif rename to Resources/Images/animB2op.gif diff --git a/screenshot.png b/Resources/Images/screenshot.png similarity index 100% rename from screenshot.png rename to Resources/Images/screenshot.png diff --git a/ResultsPreferences.py b/ResultsPreferences.py index 5d313a7..5821bd4 100644 --- a/ResultsPreferences.py +++ b/ResultsPreferences.py @@ -3,8 +3,11 @@ import FreeCAD as App import FreeCADGui from PySide import QtGui import Serialize_SearchBar +import path -genericToolIcon = QtGui.QIcon(QtGui.QIcon(os.path.dirname(__file__) + "/Tango-Tools-spanner-hammer.svg")) +ICON_LOCATION = os.path.join(os.path.dirname(__file__), "Resources", "Icons") + +genericToolIcon = QtGui.QIcon(QtGui.QIcon(os.path.join(ICON_LOCATION, "Tango-Tools-spanner-hammer.svg"))) def getParam(grpPath, type_, name): diff --git a/package.xml b/package.xml index ac7d3c0..9a7f9f1 100644 --- a/package.xml +++ b/package.xml @@ -5,7 +5,7 @@ Adds a search bar widget for tools, document objects, and preferences - 1.2.1 + 1.3.x 2022-06-01 @@ -23,7 +23,7 @@ SearchBar - Tango-System-search.svg + Resource/Icons/Tango-System-search.svg ./ search widget From 38b68fe3d7788322cf99bd98b2142bc032e8f06e Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Fri, 10 Jan 2025 15:42:44 +0100 Subject: [PATCH 13/47] Added Parameters_SearchBar.py for parametes across the addon --- Parameters_SearchBar.py | 80 +++++++++++++++++++++++++++++++++++++++++ ResultsPreferences.py | 6 ++-- ResultsRefreshTools.py | 6 ++-- SearchBox.py | 5 +-- 4 files changed, 88 insertions(+), 9 deletions(-) create mode 100644 Parameters_SearchBar.py diff --git a/Parameters_SearchBar.py b/Parameters_SearchBar.py new file mode 100644 index 0000000..33d7abb --- /dev/null +++ b/Parameters_SearchBar.py @@ -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") diff --git a/ResultsPreferences.py b/ResultsPreferences.py index 5821bd4..ff01bbe 100644 --- a/ResultsPreferences.py +++ b/ResultsPreferences.py @@ -3,11 +3,9 @@ import FreeCAD as App import FreeCADGui from PySide import QtGui import Serialize_SearchBar -import path +import Parameters_SearchBar as Parameters -ICON_LOCATION = os.path.join(os.path.dirname(__file__), "Resources", "Icons") - -genericToolIcon = QtGui.QIcon(QtGui.QIcon(os.path.join(ICON_LOCATION, "Tango-Tools-spanner-hammer.svg"))) +genericToolIcon = QtGui.QIcon(QtGui.QIcon(Parameters.genericToolIcon_Pixmap)) def getParam(grpPath, type_, name): diff --git a/ResultsRefreshTools.py b/ResultsRefreshTools.py index 0f10396..d72d900 100644 --- a/ResultsRefreshTools.py +++ b/ResultsRefreshTools.py @@ -1,6 +1,9 @@ import os from PySide import QtGui import Serialize_SearchBar +import Parameters_SearchBar as Parameters + +genericToolIcon = QtGui.QIcon(QtGui.QIcon(Parameters.genericToolIcon_Pixmap)) def refreshToolsAction(nfo): @@ -16,9 +19,6 @@ def refreshToolsToolTip(nfo, setParent): ) -genericToolIcon = QtGui.QIcon(QtGui.QIcon(os.path.dirname(__file__) + "/Tango-Tools-spanner-hammer.svg")) - - def refreshToolsResultsProvider(): return [ { diff --git a/SearchBox.py b/SearchBox.py index 78880d0..e5c2230 100644 --- a/SearchBox.py +++ b/SearchBox.py @@ -3,11 +3,12 @@ from PySide import QtGui from PySide import QtCore import FreeCADGui # just used for FreeCADGui.updateGui() from SearchBoxLight import SearchBoxLight +import Parameters_SearchBar as Parameters + +genericToolIcon = QtGui.QIcon(QtGui.QIcon(Parameters.genericToolIcon_Pixmap)) globalIgnoreFocusOut = False -genericToolIcon = QtGui.QIcon(QtGui.QIcon(os.path.dirname(__file__) + "/Tango-Tools-spanner-hammer.svg")) - def easyToolTipWidget(html): foo = QtGui.QTextEdit() From adc14d0155f304f7f40abe35a0c51a266a1d38d7 Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Fri, 10 Jan 2025 18:59:39 +0100 Subject: [PATCH 14/47] Fixed issue were click events were not working on Windows - test 1 --- SearchBox.py | 166 ++++++++++++++++++++++++++++------------------ SearchBoxLight.py | 6 ++ 2 files changed, 109 insertions(+), 63 deletions(-) diff --git a/SearchBox.py b/SearchBox.py index e5c2230..3a81a68 100644 --- a/SearchBox.py +++ b/SearchBox.py @@ -1,24 +1,59 @@ +import FreeCADGui as Gui # just used for FreeCADGui.updateGui() 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, +) +from PySide.QtGui import ( + QIcon, + QPixmap, + QColor, + QStandardItemModel, + QShortcut, + QKeySequence, + QStandardItem, +) + from SearchBoxLight import SearchBoxLight import Parameters_SearchBar as Parameters -genericToolIcon = QtGui.QIcon(QtGui.QIcon(Parameters.genericToolIcon_Pixmap)) +genericToolIcon = QIcon(Parameters.genericToolIcon_Pixmap) globalIgnoreFocusOut = False 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): +class SearchBox(QLineEdit): # The following block of code is present in the lightweight proxy SearchBoxLight """ resultSelected = QtCore.Signal(int, int) @@ -39,8 +74,8 @@ class SearchBox(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 = 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') @@ -54,25 +89,26 @@ class SearchBox(QtGui.QLineEdit): self.itemGroups = None # Will be initialized by calling getItemGroups() the first time the search box gains focus, through focusInEvent and refreshItemGroups self.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.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 = 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 @@ -80,40 +116,32 @@ class SearchBox(QtGui.QLineEdit): # Connect signals and slots self.listView.clicked.connect(lambda x: self.selectResult("select", x)) self.listView.selectionModel().selectionChanged.connect(self.onSelectionChanged) + # 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) # 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 @@ -121,6 +149,16 @@ class SearchBox(QtGui.QLineEdit): self.isInitialized = True return self + @staticmethod + def proxyMousePressEvent(self, event): + self.selectResult(mode=None, index=self.listView.currentIndex()) + return + + @staticmethod + def proxyMouseMoveEvent(self, arg__1): + self.listView.setCurrentIndex(self.listView.indexAt(arg__1.pos())) + return + @staticmethod def refreshItemGroups(self): self.itemGroups = self.getItemGroups() @@ -129,18 +167,18 @@ class SearchBox(QtGui.QLineEdit): @staticmethod def proxyFocusInEvent(self, qFocusEvent): if self.firstShowList: - mdl = QtGui.QStandardItemModel() + mdl = QStandardItemModel() mdl.appendRow( [ - QtGui.QStandardItem(genericToolIcon, "Please wait, loading results from cache…"), - QtGui.QStandardItem("0"), - QtGui.QStandardItem("-1"), + QStandardItem(genericToolIcon, "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() @@ -192,6 +230,7 @@ class SearchBox(QtGui.QLineEdit): @staticmethod def acceptKey(self, mode): + print(f"Got here, {mode}") currentIndex = self.listView.currentIndex() self.showList() if currentIndex.isValid(): @@ -220,9 +259,9 @@ class SearchBox(QtGui.QLineEdit): 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) @@ -244,8 +283,9 @@ 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]) + print(f"Got here, {index}") self.hideList() # TODO: allow other options, e.g. some items could act as combinators / cumulative filters self.setText("") @@ -281,7 +321,7 @@ class SearchBox(QtGui.QLineEdit): 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): @@ -289,9 +329,9 @@ class SearchBox(QtGui.QLineEdit): # 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"])), + QStandardItem(group["icon"] or genericToolIcon, group["text"]), + QStandardItem(str(depth)), + QStandardItem(str(group["id"])), ] ) addGroups(group["subitems"], depth + 1) @@ -314,12 +354,12 @@ class SearchBox(QtGui.QLineEdit): 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() @@ -346,7 +386,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() diff --git a/SearchBoxLight.py b/SearchBoxLight.py index 3801ded..ace81b2 100644 --- a/SearchBoxLight.py +++ b/SearchBoxLight.py @@ -41,6 +41,12 @@ class SearchBoxLight(QtGui.QLineEdit): return types.MethodType(f, self) + def MousePressEvent(self, *args, **kwargs): + return self.proxyMousePressEvent(*args, **kwargs) + + def MouseMoveEvent(self, *args, **kwargs): + return self.proxyMouseMoveEvent(*args, **kwargs) + def focusInEvent(self, *args, **kwargs): return self.proxyFocusInEvent(*args, **kwargs) From 718401b0493259f600e5a8f22c92b2014f26be75 Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Fri, 10 Jan 2025 19:22:33 +0100 Subject: [PATCH 15/47] Added translation folder + updated strings for translations --- GetItemGroups.py | 18 +++- InitGui.py | 19 +++- RefreshTools.py | 14 ++- ResultsDocument.py | 3 + ResultsRefreshTools.py | 13 ++- ResultsToolbar.py | 4 + SafeViewer.py | 22 ++-- SearchBox.py | 8 +- translations/README.md | 104 ++++++++++++++++++ translations/update_translation.sh | 166 +++++++++++++++++++++++++++++ 10 files changed, 353 insertions(+), 18 deletions(-) create mode 100644 translations/README.md create mode 100644 translations/update_translation.sh diff --git a/GetItemGroups.py b/GetItemGroups.py index 6e51a27..940e319 100644 --- a/GetItemGroups.py +++ b/GetItemGroups.py @@ -1,8 +1,14 @@ +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 @@ -17,8 +23,11 @@ def onResultSelected(index, groupId): 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.", + 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.", + ), ) @@ -31,7 +40,10 @@ def getToolTip(groupId, setParent): 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(): diff --git a/InitGui.py b/InitGui.py index 295678e..f6c577b 100644 --- a/InitGui.py +++ b/InitGui.py @@ -1,9 +1,20 @@ +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 +def QT_TRANSLATE_NOOP(context, text): + return text + + +# Define the translation +translate = App.Qt.translate + + def addToolSearchBox(): import FreeCADGui from PySide import QtGui @@ -24,9 +35,13 @@ def addToolSearchBox(): if wax is None: wax = QtGui.QWidgetAction(None) - wax.setWhatsThis("Use this search bar to find tools, document objects, preferences and more") + wax.setWhatsThis( + translate("SearchBar", "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") + sea.setWhatsThis( + translate("SearchBar", "Use this search bar to find tools, document objects, preferences and more") + ) wax.setDefaultWidget(sea) ##mbr.addWidget(sea) # mbr.addAction(wax) diff --git a/RefreshTools.py b/RefreshTools.py index b97887f..170d705 100644 --- a/RefreshTools.py +++ b/RefreshTools.py @@ -1,17 +1,20 @@ import os import FreeCAD as App +# Define the translation +translate = App.Qt.translate + def loadAllWorkbenches(): from PySide import QtGui import FreeCADGui activeWorkbench = FreeCADGui.activeWorkbench().name() - lbl = QtGui.QLabel("Loading workbench … (…/…)") + lbl = QtGui.QLabel(translate("SearchBar", "Loading workbench … (…/…)")) lbl.show() lst = FreeCADGui.listWorkbenches() for i, wb in enumerate(lst): - msg = "Loading workbench " + wb + " (" + str(i) + "/" + str(len(lst)) + ")" + msg = translate("SearchBar", "Loading workbench ") + wb + " (" + str(i) + "/" + str(len(lst)) + ")" print(msg) lbl.setText(msg) geo = lbl.geometry() @@ -88,8 +91,11 @@ def refreshToolsAction(): 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.', + 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.', + ), QtGui.QMessageBox.Yes, QtGui.QMessageBox.No, ) diff --git a/ResultsDocument.py b/ResultsDocument.py index 2e0f8a1..41b6fa0 100644 --- a/ResultsDocument.py +++ b/ResultsDocument.py @@ -5,6 +5,9 @@ import FreeCADGui import SafeViewer import SearchBox +# Define the translation +translate = App.Qt.translate + def documentAction(nfo): act = nfo["action"] diff --git a/ResultsRefreshTools.py b/ResultsRefreshTools.py index d72d900..a4267f8 100644 --- a/ResultsRefreshTools.py +++ b/ResultsRefreshTools.py @@ -1,3 +1,6 @@ +import FreeCAD as App +import FreeCADGui as Gui + import os from PySide import QtGui import Serialize_SearchBar @@ -5,6 +8,9 @@ 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 @@ -15,7 +21,12 @@ def refreshToolsAction(nfo): def refreshToolsToolTip(nfo, setParent): return ( Serialize_SearchBar.iconToHTML(genericToolIcon) - + "

    Load all workbenches to refresh this list of tools. This may take a minute, depending on the number of installed workbenches.

    " + + "

    " + + translate( + "SearchBar", + "Load all workbenches to refresh this list of tools. This may take a minute, depending on the number of installed workbenches.", + ) + + "

    " ) diff --git a/ResultsToolbar.py b/ResultsToolbar.py index ec41db2..77967a4 100644 --- a/ResultsToolbar.py +++ b/ResultsToolbar.py @@ -1,7 +1,11 @@ +import FreeCAD as App from PySide import QtGui import FreeCADGui import Serialize_SearchBar +# Define the translation +translate = App.Qt.translate + def toolbarAction(nfo): act = nfo["action"] diff --git a/SafeViewer.py b/SafeViewer.py index 2832b95..dbf5a61 100644 --- a/SafeViewer.py +++ b/SafeViewer.py @@ -1,5 +1,8 @@ from PySide import QtGui -import FreeCAD +import FreeCAD as App + +# Define the translation +translate = App.Qt.translate class SafeViewer(QtGui.QWidget): @@ -7,7 +10,7 @@ class SafeViewer(QtGui.QWidget): 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) + enabled = App.ParamGet("User parameter:BaseApp/Preferences/Mod/SearchBar").GetBool("PreviewEnabled", False) instances = [] def __init__(self, parent=None): @@ -27,11 +30,18 @@ class SafeViewer(QtGui.QWidget): 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." + 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 = 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 = 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) @@ -46,7 +56,7 @@ 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() diff --git a/SearchBox.py b/SearchBox.py index 3a81a68..45fd0be 100644 --- a/SearchBox.py +++ b/SearchBox.py @@ -1,4 +1,5 @@ -import FreeCADGui as Gui # just used for FreeCADGui.updateGui() +import FreeCAD as App +import FreeCADGui as Gui import os from PySide.QtCore import ( @@ -44,6 +45,9 @@ genericToolIcon = QIcon(Parameters.genericToolIcon_Pixmap) globalIgnoreFocusOut = False +# Define the translation +translate = App.Qt.translate + def easyToolTipWidget(html): foo = QTextEdit() @@ -170,7 +174,7 @@ class SearchBox(QLineEdit): mdl = QStandardItemModel() mdl.appendRow( [ - QStandardItem(genericToolIcon, "Please wait, loading results from cache…"), + QStandardItem(genericToolIcon, translate("SearchBar", "Please wait, loading results from cache…")), QStandardItem("0"), QStandardItem("-1"), ] diff --git a/translations/README.md b/translations/README.md new file mode 100644 index 0000000..ae84e37 --- /dev/null +++ b/translations/README.md @@ -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 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 + +``` + +to + +```xml + +``` + +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 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. + + + +## More information + +You can read more about translating external workbenches here: + + diff --git a/translations/update_translation.sh b/translations/update_translation.sh new file mode 100644 index 0000000..1422655 --- /dev/null +++ b/translations/update_translation.sh @@ -0,0 +1,166 @@ +#!/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 ] [-u ]" + echo -e "\nFlags:" + echo -e " -R\n\tRelease all translations (qm files)" + echo -e " -U\n\tUpdate all translations (ts files)" + echo -e " -r \n\tRelease the specified locale" + echo -e " -u \n\tUpdate strings for the specified locale" + echo -e " -N\n\tNormalize CrowdIn filenames" +} + +# Main function ------------------------------------------------------------------------------------ + +LUPDATE=/usr/lib/qt6/bin/lupdate # from Qt6 +# LUPDATE=lupdate # from Qt5 +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 From 5d5ef545d36104316cb81e36af4926488fd4cfb7 Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Fri, 10 Jan 2025 19:26:12 +0100 Subject: [PATCH 16/47] Moved translate inside def --- InitGui.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/InitGui.py b/InitGui.py index f6c577b..66ad11d 100644 --- a/InitGui.py +++ b/InitGui.py @@ -11,15 +11,14 @@ def QT_TRANSLATE_NOOP(context, text): return text -# Define the translation -translate = App.Qt.translate - - def addToolSearchBox(): import FreeCADGui from PySide import QtGui import SearchBoxLight + # Define the translation + translate = App.Qt.translate + global wax, sea, tbr mw = FreeCADGui.getMainWindow() if mw: From 4f63352360fb7b083f721735947b12126a013733 Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Fri, 10 Jan 2025 19:29:19 +0100 Subject: [PATCH 17/47] added precommit files --- .pre-commit-config.yaml | 16 ++++++++++++++++ .pre-commit-search-and-replace.yaml | 6 ++++++ 2 files changed, 22 insertions(+) create mode 100644 .pre-commit-config.yaml create mode 100644 .pre-commit-search-and-replace.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..13a3961 --- /dev/null +++ b/.pre-commit-config.yaml @@ -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 diff --git a/.pre-commit-search-and-replace.yaml b/.pre-commit-search-and-replace.yaml new file mode 100644 index 0000000..383d55e --- /dev/null +++ b/.pre-commit-search-and-replace.yaml @@ -0,0 +1,6 @@ +- search: PySide6 + replacement: PySide +- search: PySide2 + replacement: PySide +- search: pyqtribbon as + replacement: pyqtribbon_local as From ba2471e592848e2a07f9cb6aaa3b9146e417021a Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Fri, 10 Jan 2025 19:30:36 +0100 Subject: [PATCH 18/47] updated .gitignore --- .gitignore | 142 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 141 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6cb10c8..af3b66b 100644 --- a/.gitignore +++ b/.gitignore @@ -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 From f35228c43f038c48d8dcd0242a0591f3eb90f24d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 10 Jan 2025 18:32:25 +0000 Subject: [PATCH 19/47] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- BuiltInSearchResults.py | 116 ++++++++++++++++-------- InitGui.py | 22 ++++- README.md | 4 +- RefreshTools.py | 10 +- Resources/Icons/Tango-System-search.svg | 2 +- ResultsDocument.py | 25 ++++- ResultsPreferences.py | 7 +- ResultsToolbar.py | 45 +++++++-- SafeViewer.py | 23 +++-- SearchBox.py | 69 ++++++++++---- SearchBoxLight.py | 8 +- SearchResults.py | 18 ++-- Serialize_SearchBar.py | 26 +++++- package.xml | 22 ++--- 14 files changed, 291 insertions(+), 106 deletions(-) diff --git a/BuiltInSearchResults.py b/BuiltInSearchResults.py index c35be2d..50af4cb 100644 --- a/BuiltInSearchResults.py +++ b/BuiltInSearchResults.py @@ -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 + ), +) diff --git a/InitGui.py b/InitGui.py index 66ad11d..bd5ca57 100644 --- a/InitGui.py +++ b/InitGui.py @@ -25,21 +25,33 @@ def addToolSearchBox(): 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(), + 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) + lambda index, groupId: __import__("GetItemGroups").onResultSelected( + index, groupId + ) ) if wax is None: wax = QtGui.QWidgetAction(None) wax.setWhatsThis( - translate("SearchBar", "Use this search bar to find tools, document objects, preferences and more") + 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") + translate( + "SearchBar", + "Use this search bar to find tools, document objects, preferences and more", + ) ) wax.setDefaultWidget(sea) ##mbr.addWidget(sea) diff --git a/README.md b/README.md index 9a7862c..066c948 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ https://github.com/SuzanneSoy/SearchBar/issues/12 is fixed. #### Automatic Install -Install **SearchBar** addon via the FreeCAD Addon Manager from the **Tools** :arrow_right: **Addon Manager** dropdown menu. +Install **SearchBar** addon via the FreeCAD Addon Manager from the **Tools** :arrow_right: **Addon Manager** dropdown menu. #### Manual Install @@ -64,7 +64,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/APEbbers/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). diff --git a/RefreshTools.py b/RefreshTools.py index 170d705..0f5db32 100644 --- a/RefreshTools.py +++ b/RefreshTools.py @@ -14,7 +14,15 @@ def loadAllWorkbenches(): lbl.show() lst = FreeCADGui.listWorkbenches() for i, wb in enumerate(lst): - msg = translate("SearchBar", "Loading workbench ") + wb + " (" + str(i) + "/" + str(len(lst)) + ")" + msg = ( + translate("SearchBar", "Loading workbench ") + + wb + + " (" + + str(i) + + "/" + + str(len(lst)) + + ")" + ) print(msg) lbl.setText(msg) geo = lbl.geometry() diff --git a/Resources/Icons/Tango-System-search.svg b/Resources/Icons/Tango-System-search.svg index f98b2ea..192bc55 100644 --- a/Resources/Icons/Tango-System-search.svg +++ b/Resources/Icons/Tango-System-search.svg @@ -43,4 +43,4 @@ - \ No newline at end of file + diff --git a/ResultsDocument.py b/ResultsDocument.py index 41b6fa0..0d98edb 100644 --- a/ResultsDocument.py +++ b/ResultsDocument.py @@ -61,9 +61,14 @@ class DocumentObjectToolTipWidget(QtGui.QWidget): # Tried setting the preview to a fixed size to prevent it from disappearing when changing its contents, this sets it to a fixed size but doesn't actually pick the size, .resize does that but isn't enough to fix the bug. # 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. @@ -115,12 +120,22 @@ def documentResultsProvider(): group = [] for o in doc.Objects: # all_actions.append(lambda: ) - action = {"handler": "documentObject", "document": o.Document.Name, "object": o.Name} + action = { + "handler": "documentObject", + "document": o.Document.Name, + "object": o.Name, + } item = { - "icon": o.ViewObject.Icon if o.ViewObject and o.ViewObject.Icon else None, + "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}, + "toolTip": { + "label": o.Label, + "name": o.Name, + "docName": o.Document.Name, + }, "action": action, "subitems": [], } diff --git a/ResultsPreferences.py b/ResultsPreferences.py index ff01bbe..4079c1d 100644 --- a/ResultsPreferences.py +++ b/ResultsPreferences.py @@ -42,7 +42,12 @@ def getParamGroups(nameInConfig, nameInPath): 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) + recur( + False, + path + (":" if atRoot else "/") + child.attrib["Name"], + child.attrib["Name"], + child, + ) for child in tree.getchildren() if child.tag == "FCParamGroup" ] diff --git a/ResultsToolbar.py b/ResultsToolbar.py index 77967a4..ac49aca 100644 --- a/ResultsToolbar.py +++ b/ResultsToolbar.py @@ -9,7 +9,12 @@ translate = App.Qt.translate def toolbarAction(nfo): act = nfo["action"] - print("show toolbar " + act["toolbar"] + " from workbenches " + repr(act["workbenches"])) + print( + "show toolbar " + + act["toolbar"] + + " from workbenches " + + repr(act["workbenches"]) + ) def subToolAction(nfo): @@ -45,7 +50,10 @@ def subToolAction(nfo): return True elif action is not None: print( - "Run action of tool " + toolPath + " available in workbenches " + repr(act["workbenches"]) + "Run action of tool " + + toolPath + + " available in workbenches " + + repr(act["workbenches"]) ) action.trigger() return True @@ -59,14 +67,22 @@ def subToolAction(nfo): FreeCADGui.activateWorkbench(workbench) 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 = [ "
  • " - + (Serialize_SearchBar.iconToHTML(QtGui.QIcon(workbenches[wb].Icon)) if wb in workbenches else "? ") + + ( + Serialize_SearchBar.iconToHTML(QtGui.QIcon(workbenches[wb].Icon)) + if wb in workbenches + else "? " + ) + wb + "
  • " for wb in nfo["action"]["workbenches"] @@ -81,7 +97,12 @@ def toolbarToolTip(nfo, setParent): def subToolToolTip(nfo, setParent): - return Serialize_SearchBar.iconToHTML(nfo["icon"], 32) + "

    " + nfo["toolTip"] + "

    " + return ( + Serialize_SearchBar.iconToHTML(nfo["icon"], 32) + + "

    " + + nfo["toolTip"] + + "

    " + ) def getAllToolbars(): @@ -144,10 +165,20 @@ def toolbarResultsProvider(): "showMenu": bool(men), } group.append( - {"icon": icon, "text": text, "toolTip": tbt.toolTip(), "action": action, "subitems": subgroup} + { + "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} + action = { + "handler": "toolbar", + "workbenches": toolbarIsInWorkbenches, + "toolbar": toolbarName, + } itemGroups.append( { "icon": QtGui.QIcon(":/icons/Group.svg"), diff --git a/SafeViewer.py b/SafeViewer.py index dbf5a61..1154dc3 100644 --- a/SafeViewer.py +++ b/SafeViewer.py @@ -8,9 +8,12 @@ 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.""" + 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) + enabled = App.ParamGet("User parameter:BaseApp/Preferences/Mod/SearchBar").GetBool( + "PreviewEnabled", False + ) instances = [] def __init__(self, parent=None): @@ -38,11 +41,15 @@ class SafeViewer(QtGui.QWidget): 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_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.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) @@ -56,7 +63,9 @@ class SafeViewer(QtGui.QWidget): def enable_for_future_sessions(self): if not SafeViewer.enabled: # Store in prefs - App.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() @@ -77,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() diff --git a/SearchBox.py b/SearchBox.py index 45fd0be..b5633b1 100644 --- a/SearchBox.py +++ b/SearchBox.py @@ -91,7 +91,9 @@ class SearchBox(QLineEdit): 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 + self.maxVisibleRows = ( + maxVisibleRows # TODO: use this to compute the correct height + ) # Create proxy model 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. @@ -103,7 +105,9 @@ class SearchBox(QLineEdit): 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.setItemDelegate( + getItemDelegate() + ) # https://stackoverflow.com/a/65930408/324969 self.listView.setMouseTracking(True) # make the QListView non-editable self.listView.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) @@ -128,10 +132,18 @@ class SearchBox(QLineEdit): # Note: should probably use the eventFilter method instead... wdgctx = Qt.ShortcutContext.WidgetShortcut - 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) + 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. # QShortcut(QKeySequence.MoveToEndOfDocument, self, context = wdgctx).activated.connect(self.listEnd) @@ -139,13 +151,25 @@ class SearchBox(QLineEdit): # QShortcut(QKeySequence(Qt.Key.Key_End), self, context = wdgctx).activated.connect(self.listEnd) # QShortcut(QKeySequence('Home'), self, context = wdgctx).activated.connect(self.listStart) - 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) + 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 + ) - QShortcut(QKeySequence(Qt.Key.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 @@ -174,7 +198,12 @@ class SearchBox(QLineEdit): mdl = QStandardItemModel() mdl.appendRow( [ - QStandardItem(genericToolIcon, translate("SearchBar", "Please wait, loading results from cache…")), + QStandardItem( + genericToolIcon, + translate( + "SearchBar", "Please wait, loading results from cache…" + ), + ), QStandardItem("0"), QStandardItem("-1"), ] @@ -218,11 +247,17 @@ class SearchBox(QLineEdit): @staticmethod def proxyListPageDown(self): - self.movementKey(lambda current, nbRows: min(current + max(1, self.maxVisibleRows / 2), nbRows - 1)) + 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)) + self.movementKey( + lambda current, nbRows: max(current - max(1, self.maxVisibleRows / 2), 0) + ) @staticmethod def proxyListEnd(self): @@ -358,7 +393,9 @@ class SearchBox(QLineEdit): def getScreenPosition(widget): geo = widget.geometry() parent = widget.parent() - parentPos = getScreenPosition(parent) if parent is not None else QPoint(0, 0) + 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) diff --git a/SearchBoxLight.py b/SearchBoxLight.py index ace81b2..90c4a23 100644 --- a/SearchBoxLight.py +++ b/SearchBoxLight.py @@ -6,7 +6,9 @@ from PySide import QtCore 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 @@ -25,7 +27,9 @@ class SearchBoxLight(QtGui.QLineEdit): 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.setFixedWidth( + 200 + ) # needed to avoid a change of width when the clear button appears/disappears def lazyInit(self): pass diff --git a/SearchResults.py b/SearchResults.py index 58252fa..70e8ada 100644 --- a/SearchResults.py +++ b/SearchResults.py @@ -1,18 +1,20 @@ -actionHandlers = { } -toolTipHandlers = { } -resultProvidersCached = { } -resultProvidersUncached = { } +actionHandlers = {} +toolTipHandlers = {} +resultProvidersCached = {} +resultProvidersUncached = {} + # name : string # getItemGroupsCached: () -> [itemGroup] # getItemGroupsUncached: () -> [itemGroup] def registerResultProvider(name, getItemGroupsCached, getItemGroupsUncached): - resultProvidersCached[name] = getItemGroupsCached - resultProvidersUncached[name] = getItemGroupsUncached + resultProvidersCached[name] = getItemGroupsCached + resultProvidersUncached[name] = getItemGroupsUncached + # name : str # action : act -> None # toolTip : groupId, setParent -> (str or QWidget) def registerResultHandler(name, action, toolTip): - actionHandlers[name] = action - toolTipHandlers[name] = toolTip + actionHandlers[name] = action + toolTipHandlers[name] = toolTip diff --git a/Serialize_SearchBar.py b/Serialize_SearchBar.py index e2c0847..86a498f 100644 --- a/Serialize_SearchBar.py +++ b/Serialize_SearchBar.py @@ -3,7 +3,12 @@ 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): +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. @@ -59,8 +64,13 @@ def serializeIcon(icon): "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) + 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 @@ -87,9 +97,15 @@ def deserializeIcon(iconPixmaps): "selected": QtGui.QIcon.Mode.Selected, }[strMode] for strState, statePixmap in modePixmaps.items(): - state = {"off": QtGui.QIcon.State.Off, "on": QtGui.QIcon.State.On}[strState] + 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")))) + pxm.loadFromData( + QtCore.QByteArray.fromBase64( + bytearray(statePixmap.encode("utf-8")) + ) + ) ico.addPixmap(pxm, mode, state) return ico diff --git a/package.xml b/package.xml index 9a7f9f1..76cebb9 100644 --- a/package.xml +++ b/package.xml @@ -1,24 +1,24 @@ - + SearchBar - + Adds a search bar widget for tools, document objects, and preferences - + 1.3.x - + 2022-06-01 - + Paul Ebbers - + CCOv1 - + https://github.com/APEbbers/SearchBar - + https://github.com/APEbbers/SearchBar/issues - + https://github.com/APEbbers/SearchBar - + lxml @@ -30,5 +30,5 @@ ui/ux - + From 5fbeab5eb3caf2caba7ec777fe679cbcda1a1ead Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Fri, 10 Jan 2025 19:33:03 +0100 Subject: [PATCH 20/47] updated version --- package.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.xml b/package.xml index 76cebb9..5d7f553 100644 --- a/package.xml +++ b/package.xml @@ -5,7 +5,7 @@ Adds a search bar widget for tools, document objects, and preferences - 1.3.x + 1.3.0 2022-06-01 From ebbb39ab00b28fe935260fd8fa7c7f1f8a875810 Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Sat, 11 Jan 2025 13:27:22 +0100 Subject: [PATCH 21/47] Improvement on selection + Fix iwth copy from contextmenu --- SearchBox.py | 121 ++++++++++++++++++---------------------------- SearchBoxLight.py | 15 +++--- package.xml | 2 +- 3 files changed, 54 insertions(+), 84 deletions(-) diff --git a/SearchBox.py b/SearchBox.py index b5633b1..ca52129 100644 --- a/SearchBox.py +++ b/SearchBox.py @@ -2,14 +2,14 @@ import FreeCAD as App import FreeCADGui as Gui import os -from PySide.QtCore import ( +from PySide6.QtCore import ( Qt, SIGNAL, QSize, QIdentityProxyModel, QPoint, ) -from PySide.QtWidgets import ( +from PySide6.QtWidgets import ( QTabWidget, QSlider, QSpinBox, @@ -28,7 +28,7 @@ from PySide.QtWidgets import ( QApplication, QListWidget, ) -from PySide.QtGui import ( +from PySide6.QtGui import ( QIcon, QPixmap, QColor, @@ -91,9 +91,7 @@ class SearchBox(QLineEdit): 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 - ) + self.maxVisibleRows = maxVisibleRows # TODO: use this to compute the correct height # Create proxy model 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. @@ -105,9 +103,7 @@ class SearchBox(QLineEdit): 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.setItemDelegate(getItemDelegate()) # https://stackoverflow.com/a/65930408/324969 self.listView.setMouseTracking(True) # make the QListView non-editable self.listView.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) @@ -122,28 +118,19 @@ class SearchBox(QLineEdit): 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) # 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 = Qt.ShortcutContext.WidgetShortcut - 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) + 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. # QShortcut(QKeySequence.MoveToEndOfDocument, self, context = wdgctx).activated.connect(self.listEnd) @@ -151,25 +138,13 @@ class SearchBox(QLineEdit): # QShortcut(QKeySequence(Qt.Key.Key_End), self, context = wdgctx).activated.connect(self.listEnd) # QShortcut(QKeySequence('Home'), self, context = wdgctx).activated.connect(self.listStart) - 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 - ) + 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) - QShortcut( - QKeySequence(Qt.Key.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 @@ -179,12 +154,23 @@ class SearchBox(QLineEdit): @staticmethod def proxyMousePressEvent(self, event): - self.selectResult(mode=None, index=self.listView.currentIndex()) + if self.listView.underMouse(): + self.selectResult(mode=None, index=self.listView.currentIndex()) + else: + event.ignore() return @staticmethod def proxyMouseMoveEvent(self, arg__1): - self.listView.setCurrentIndex(self.listView.indexAt(arg__1.pos())) + 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 @@ -194,15 +180,18 @@ class SearchBox(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 = QStandardItemModel() mdl.appendRow( [ QStandardItem( genericToolIcon, - translate( - "SearchBar", "Please wait, loading results from cache…" - ), + translate("SearchBar", "Please wait, loading results from cache…"), ), QStandardItem("0"), QStandardItem("-1"), @@ -217,13 +206,20 @@ class SearchBox(QLineEdit): 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): @@ -247,17 +243,11 @@ class SearchBox(QLineEdit): @staticmethod def proxyListPageDown(self): - self.movementKey( - lambda current, nbRows: min( - current + max(1, self.maxVisibleRows / 2), nbRows - 1 - ) - ) + 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) - ) + self.movementKey(lambda current, nbRows: max(current - max(1, self.maxVisibleRows / 2), 0)) @staticmethod def proxyListEnd(self): @@ -324,7 +314,6 @@ class SearchBox(QLineEdit): @staticmethod def selectResult(self, mode: None, index): groupId = int(index.model().itemData(index.siblingAtColumn(2))[0]) - print(f"Got here, {index}") self.hideList() # TODO: allow other options, e.g. some items could act as combinators / cumulative filters self.setText("") @@ -393,9 +382,7 @@ class SearchBox(QLineEdit): def getScreenPosition(widget): geo = widget.geometry() parent = widget.parent() - parentPos = ( - getScreenPosition(parent) if parent is not None else QPoint(0, 0) - ) + 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) @@ -425,22 +412,6 @@ class SearchBox(QLineEdit): self.listView.setGeometry(x, y, w, h) self.extraInfo.setGeometry(extrax, y, extraw, h) - @staticmethod - def proxyOnSelectionChanged(self, selected, deselected): - # 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() - deselected = deselected.indexes() - if len(selected) > 0: - index = selected[0] - self.setExtraInfo(index) - # Poor attempt to circumvent a glitch where the extra info pane stays visible after pressing Return - if not self.listView.isHidden(): - self.showExtraInfo() - elif len(deselected) > 0: - self.hideExtraInfo() - @staticmethod def setExtraInfo(self, index): if self.currentExtraInfo == (index.row(), index.column(), index.model()): diff --git a/SearchBoxLight.py b/SearchBoxLight.py index 90c4a23..0fd8310 100644 --- a/SearchBoxLight.py +++ b/SearchBoxLight.py @@ -6,9 +6,7 @@ from PySide import QtCore 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 @@ -27,9 +25,7 @@ class SearchBoxLight(QtGui.QLineEdit): 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.setFixedWidth(200) # needed to avoid a change of width when the clear button appears/disappears def lazyInit(self): pass @@ -51,6 +47,9 @@ class SearchBoxLight(QtGui.QLineEdit): 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) @@ -60,8 +59,8 @@ class SearchBoxLight(QtGui.QLineEdit): def keyPressEvent(self, *args, **kwargs): return self.proxyKeyPressEvent(*args, **kwargs) - def onSelectionChanged(self, *args, **kwargs): - return self.proxyOnSelectionChanged(*args, **kwargs) + # def onSelectionChanged(self, *args, **kwargs): + # return self.proxyOnSelectionChanged(*args, **kwargs) def filterModel(self, *args, **kwargs): return self.proxyFilterModel(*args, **kwargs) diff --git a/package.xml b/package.xml index 5d7f553..5bebba0 100644 --- a/package.xml +++ b/package.xml @@ -5,7 +5,7 @@ Adds a search bar widget for tools, document objects, and preferences - 1.3.0 + 1.3.1 2022-06-01 From 92526c9b65a6afe6462bde717102c31cf0238f7c Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Sat, 11 Jan 2025 13:30:57 +0100 Subject: [PATCH 22/47] Fixed PySide6 import --- SearchBox.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SearchBox.py b/SearchBox.py index ca52129..26ee4a3 100644 --- a/SearchBox.py +++ b/SearchBox.py @@ -2,14 +2,14 @@ import FreeCAD as App import FreeCADGui as Gui import os -from PySide6.QtCore import ( +from PySide.QtCore import ( Qt, SIGNAL, QSize, QIdentityProxyModel, QPoint, ) -from PySide6.QtWidgets import ( +from PySide.QtWidgets import ( QTabWidget, QSlider, QSpinBox, @@ -28,7 +28,7 @@ from PySide6.QtWidgets import ( QApplication, QListWidget, ) -from PySide6.QtGui import ( +from PySide.QtGui import ( QIcon, QPixmap, QColor, From 3ab85867d9c12a27c395e0fb148ec78d2889059c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 12:33:44 +0000 Subject: [PATCH 23/47] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- SearchBox.py | 66 +++++++++++++++++++++++++++++++++++------------ SearchBoxLight.py | 8 ++++-- 2 files changed, 56 insertions(+), 18 deletions(-) diff --git a/SearchBox.py b/SearchBox.py index 26ee4a3..32a7904 100644 --- a/SearchBox.py +++ b/SearchBox.py @@ -91,7 +91,9 @@ class SearchBox(QLineEdit): 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 + self.maxVisibleRows = ( + maxVisibleRows # TODO: use this to compute the correct height + ) # Create proxy model 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. @@ -103,7 +105,9 @@ class SearchBox(QLineEdit): 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.setItemDelegate( + getItemDelegate() + ) # https://stackoverflow.com/a/65930408/324969 self.listView.setMouseTracking(True) # make the QListView non-editable self.listView.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) @@ -127,10 +131,18 @@ class SearchBox(QLineEdit): # Note: should probably use the eventFilter method instead... wdgctx = Qt.ShortcutContext.WidgetShortcut - 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) + 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. # QShortcut(QKeySequence.MoveToEndOfDocument, self, context = wdgctx).activated.connect(self.listEnd) @@ -138,13 +150,25 @@ class SearchBox(QLineEdit): # QShortcut(QKeySequence(Qt.Key.Key_End), self, context = wdgctx).activated.connect(self.listEnd) # QShortcut(QKeySequence('Home'), self, context = wdgctx).activated.connect(self.listStart) - 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) + 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 + ) - QShortcut(QKeySequence(Qt.Key.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 @@ -191,7 +215,9 @@ class SearchBox(QLineEdit): [ QStandardItem( genericToolIcon, - translate("SearchBar", "Please wait, loading results from cache…"), + translate( + "SearchBar", "Please wait, loading results from cache…" + ), ), QStandardItem("0"), QStandardItem("-1"), @@ -243,11 +269,17 @@ class SearchBox(QLineEdit): @staticmethod def proxyListPageDown(self): - self.movementKey(lambda current, nbRows: min(current + max(1, self.maxVisibleRows / 2), nbRows - 1)) + 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)) + self.movementKey( + lambda current, nbRows: max(current - max(1, self.maxVisibleRows / 2), 0) + ) @staticmethod def proxyListEnd(self): @@ -382,7 +414,9 @@ class SearchBox(QLineEdit): def getScreenPosition(widget): geo = widget.geometry() parent = widget.parent() - parentPos = getScreenPosition(parent) if parent is not None else QPoint(0, 0) + 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) diff --git a/SearchBoxLight.py b/SearchBoxLight.py index 0fd8310..9a32661 100644 --- a/SearchBoxLight.py +++ b/SearchBoxLight.py @@ -6,7 +6,9 @@ from PySide import QtCore 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 @@ -25,7 +27,9 @@ class SearchBoxLight(QtGui.QLineEdit): 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.setFixedWidth( + 200 + ) # needed to avoid a change of width when the clear button appears/disappears def lazyInit(self): pass From 9438e130518d7e45a5ca64b00382e3a8a00c18f3 Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Sat, 11 Jan 2025 13:34:43 +0100 Subject: [PATCH 24/47] Update README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 066c948..d237003 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,7 @@ have provided that tool. ![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](Resources/Images/animB2op.gif) From 07ff7a88bb4a62a0cb9ddb541ecc3d5e50597566 Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Sat, 11 Jan 2025 13:57:41 +0100 Subject: [PATCH 25/47] Fixed issue were not all wb's were shown --- SearchBox.py | 3 ++- package.xml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/SearchBox.py b/SearchBox.py index 26ee4a3..37bceb9 100644 --- a/SearchBox.py +++ b/SearchBox.py @@ -118,6 +118,8 @@ class SearchBox(QLineEdit): 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) # 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) @@ -259,7 +261,6 @@ class SearchBox(QLineEdit): @staticmethod def acceptKey(self, mode): - print(f"Got here, {mode}") currentIndex = self.listView.currentIndex() self.showList() if currentIndex.isValid(): diff --git a/package.xml b/package.xml index 5bebba0..0b0e04a 100644 --- a/package.xml +++ b/package.xml @@ -5,7 +5,7 @@ Adds a search bar widget for tools, document objects, and preferences - 1.3.1 + 1.3.1.1 2022-06-01 From f8b36b25c1e85ce9053ca1d626b1e24dedbf41d2 Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Sat, 11 Jan 2025 14:16:58 +0100 Subject: [PATCH 26/47] Added a try-catch to avoid errors due disabled workbenches --- ResultsToolbar.py | 37 ++++++++++--------------------------- package.xml | 2 +- 2 files changed, 11 insertions(+), 28 deletions(-) diff --git a/ResultsToolbar.py b/ResultsToolbar.py index ac49aca..8f477d4 100644 --- a/ResultsToolbar.py +++ b/ResultsToolbar.py @@ -9,12 +9,7 @@ translate = App.Qt.translate def toolbarAction(nfo): act = nfo["action"] - print( - "show toolbar " - + act["toolbar"] - + " from workbenches " - + repr(act["workbenches"]) - ) + print("show toolbar " + act["toolbar"] + " from workbenches " + repr(act["workbenches"])) def subToolAction(nfo): @@ -50,10 +45,7 @@ def subToolAction(nfo): return True elif action is not None: print( - "Run action of tool " - + toolPath - + " available in workbenches " - + repr(act["workbenches"]) + "Run action of tool " + toolPath + " available in workbenches " + repr(act["workbenches"]) ) action.trigger() return True @@ -64,25 +56,21 @@ def subToolAction(nfo): else: for workbench in act["workbenches"]: print("Activating workbench " + workbench + " to access tool " + toolPath) - FreeCADGui.activateWorkbench(workbench) + try: + FreeCADGui.activateWorkbench(workbench) + except Exception: + print("Workbench not present") + 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 = [ "
  • " - + ( - Serialize_SearchBar.iconToHTML(QtGui.QIcon(workbenches[wb].Icon)) - if wb in workbenches - else "? " - ) + + (Serialize_SearchBar.iconToHTML(QtGui.QIcon(workbenches[wb].Icon)) if wb in workbenches else "? ") + wb + "
  • " for wb in nfo["action"]["workbenches"] @@ -97,12 +85,7 @@ def toolbarToolTip(nfo, setParent): def subToolToolTip(nfo, setParent): - return ( - Serialize_SearchBar.iconToHTML(nfo["icon"], 32) - + "

    " - + nfo["toolTip"] - + "

    " - ) + return Serialize_SearchBar.iconToHTML(nfo["icon"], 32) + "

    " + nfo["toolTip"] + "

    " def getAllToolbars(): diff --git a/package.xml b/package.xml index 0b0e04a..c6e15d8 100644 --- a/package.xml +++ b/package.xml @@ -5,7 +5,7 @@ Adds a search bar widget for tools, document objects, and preferences - 1.3.1.1 + 1.3.1.2 2022-06-01 From a1e4c817d4a476242f036b040a83437ffb783cbc Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Sat, 11 Jan 2025 14:19:38 +0100 Subject: [PATCH 27/47] Update message --- ResultsToolbar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ResultsToolbar.py b/ResultsToolbar.py index 8f477d4..be8ad1d 100644 --- a/ResultsToolbar.py +++ b/ResultsToolbar.py @@ -59,7 +59,7 @@ def subToolAction(nfo): try: FreeCADGui.activateWorkbench(workbench) except Exception: - print("Workbench not present") + print("SearchBar: Workbench not present! Was it disabled?") return if runTool(): return From 906f90739421b839153c901fa1d8de1a8ebc2ba9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 13:20:51 +0000 Subject: [PATCH 28/47] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- ResultsToolbar.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/ResultsToolbar.py b/ResultsToolbar.py index be8ad1d..d179a22 100644 --- a/ResultsToolbar.py +++ b/ResultsToolbar.py @@ -9,7 +9,12 @@ translate = App.Qt.translate def toolbarAction(nfo): act = nfo["action"] - print("show toolbar " + act["toolbar"] + " from workbenches " + repr(act["workbenches"])) + print( + "show toolbar " + + act["toolbar"] + + " from workbenches " + + repr(act["workbenches"]) + ) def subToolAction(nfo): @@ -45,7 +50,10 @@ def subToolAction(nfo): return True elif action is not None: print( - "Run action of tool " + toolPath + " available in workbenches " + repr(act["workbenches"]) + "Run action of tool " + + toolPath + + " available in workbenches " + + repr(act["workbenches"]) ) action.trigger() return True @@ -63,14 +71,22 @@ def subToolAction(nfo): 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 = [ "
  • " - + (Serialize_SearchBar.iconToHTML(QtGui.QIcon(workbenches[wb].Icon)) if wb in workbenches else "? ") + + ( + Serialize_SearchBar.iconToHTML(QtGui.QIcon(workbenches[wb].Icon)) + if wb in workbenches + else "? " + ) + wb + "
  • " for wb in nfo["action"]["workbenches"] @@ -85,7 +101,12 @@ def toolbarToolTip(nfo, setParent): def subToolToolTip(nfo, setParent): - return Serialize_SearchBar.iconToHTML(nfo["icon"], 32) + "

    " + nfo["toolTip"] + "

    " + return ( + Serialize_SearchBar.iconToHTML(nfo["icon"], 32) + + "

    " + + nfo["toolTip"] + + "

    " + ) def getAllToolbars(): From b06f6bd460f12347e20d5eb8c26be6d50c3c2550 Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Sat, 11 Jan 2025 15:44:27 +0100 Subject: [PATCH 29/47] Updated text --- RefreshTools.py | 12 +----- ResultsRefreshTools.py | 4 +- SearchBox.py | 90 ++++++++++++++++++------------------------ SearchBoxLight.py | 12 ++---- 4 files changed, 46 insertions(+), 72 deletions(-) diff --git a/RefreshTools.py b/RefreshTools.py index 0f5db32..5caf2c7 100644 --- a/RefreshTools.py +++ b/RefreshTools.py @@ -14,15 +14,7 @@ def loadAllWorkbenches(): lbl.show() lst = FreeCADGui.listWorkbenches() for i, wb in enumerate(lst): - msg = ( - translate("SearchBar", "Loading workbench ") - + wb - + " (" - + str(i) - + "/" - + str(len(lst)) - + ")" - ) + msg = translate("SearchBar", "Loading workbench ") + wb + " (" + str(i) + "/" + str(len(lst)) + ")" print(msg) lbl.setText(msg) geo = lbl.geometry() @@ -102,7 +94,7 @@ def refreshToolsAction(): 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.', + """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, diff --git a/ResultsRefreshTools.py b/ResultsRefreshTools.py index a4267f8..9ac924f 100644 --- a/ResultsRefreshTools.py +++ b/ResultsRefreshTools.py @@ -24,7 +24,7 @@ def refreshToolsToolTip(nfo, setParent): + "

    " + translate( "SearchBar", - "Load all workbenches to refresh this list of tools. This may take a minute, depending on the number of installed workbenches.", + "Load all workbenches to refresh the cached results. This may take a minute, depending on the number of installed workbenches.", ) + "

    " ) @@ -34,7 +34,7 @@ def refreshToolsResultsProvider(): return [ { "icon": genericToolIcon, - "text": "Refresh list of tools", + "text": translate("SearchBar", "Refresh cached results"), "toolTip": "", "action": {"handler": "refreshTools"}, "subitems": [], diff --git a/SearchBox.py b/SearchBox.py index 8e44b47..492f1f3 100644 --- a/SearchBox.py +++ b/SearchBox.py @@ -91,9 +91,7 @@ class SearchBox(QLineEdit): 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 - ) + self.maxVisibleRows = maxVisibleRows # TODO: use this to compute the correct height # Create proxy model 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. @@ -105,9 +103,7 @@ class SearchBox(QLineEdit): 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.setItemDelegate(getItemDelegate()) # https://stackoverflow.com/a/65930408/324969 self.listView.setMouseTracking(True) # make the QListView non-editable self.listView.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) @@ -122,8 +118,12 @@ class SearchBox(QLineEdit): 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) @@ -133,18 +133,10 @@ class SearchBox(QLineEdit): # Note: should probably use the eventFilter method instead... wdgctx = Qt.ShortcutContext.WidgetShortcut - 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) + 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. # QShortcut(QKeySequence.MoveToEndOfDocument, self, context = wdgctx).activated.connect(self.listEnd) @@ -152,25 +144,13 @@ class SearchBox(QLineEdit): # QShortcut(QKeySequence(Qt.Key.Key_End), self, context = wdgctx).activated.connect(self.listEnd) # QShortcut(QKeySequence('Home'), self, context = wdgctx).activated.connect(self.listStart) - 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 - ) + 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) - QShortcut( - QKeySequence(Qt.Key.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 @@ -217,9 +197,7 @@ class SearchBox(QLineEdit): [ QStandardItem( genericToolIcon, - translate( - "SearchBar", "Please wait, loading results from cache…" - ), + translate("SearchBar", "Please wait, loading results from cache…"), ), QStandardItem("0"), QStandardItem("-1"), @@ -271,17 +249,11 @@ class SearchBox(QLineEdit): @staticmethod def proxyListPageDown(self): - self.movementKey( - lambda current, nbRows: min( - current + max(1, self.maxVisibleRows / 2), nbRows - 1 - ) - ) + 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) - ) + self.movementKey(lambda current, nbRows: max(current - max(1, self.maxVisibleRows / 2), 0)) @staticmethod def proxyListEnd(self): @@ -415,9 +387,7 @@ class SearchBox(QLineEdit): def getScreenPosition(widget): geo = widget.geometry() parent = widget.parent() - parentPos = ( - getScreenPosition(parent) if parent is not None else QPoint(0, 0) - ) + 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) @@ -447,6 +417,22 @@ class SearchBox(QLineEdit): self.listView.setGeometry(x, y, w, h) self.extraInfo.setGeometry(extrax, y, extraw, h) + @staticmethod + def proxyOnSelectionChanged(self, selected, deselected): + # 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() + deselected = deselected.indexes() + if len(selected) > 0: + index = selected[0] + self.setExtraInfo(index) + # Poor attempt to circumvent a glitch where the extra info pane stays visible after pressing Return + if not self.listView.isHidden(): + self.showExtraInfo() + elif len(deselected) > 0: + self.hideExtraInfo() + @staticmethod def setExtraInfo(self, index): if self.currentExtraInfo == (index.row(), index.column(), index.model()): diff --git a/SearchBoxLight.py b/SearchBoxLight.py index 9a32661..81a1e6a 100644 --- a/SearchBoxLight.py +++ b/SearchBoxLight.py @@ -6,9 +6,7 @@ from PySide import QtCore 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 @@ -27,9 +25,7 @@ class SearchBoxLight(QtGui.QLineEdit): 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.setFixedWidth(200) # needed to avoid a change of width when the clear button appears/disappears def lazyInit(self): pass @@ -63,8 +59,8 @@ class SearchBoxLight(QtGui.QLineEdit): def keyPressEvent(self, *args, **kwargs): return self.proxyKeyPressEvent(*args, **kwargs) - # def onSelectionChanged(self, *args, **kwargs): - # return self.proxyOnSelectionChanged(*args, **kwargs) + def onSelectionChanged(self, *args, **kwargs): + return self.proxyOnSelectionChanged(*args, **kwargs) def filterModel(self, *args, **kwargs): return self.proxyFilterModel(*args, **kwargs) From 9752e992dc7abe46246b3da4a212507233708dd6 Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Sat, 11 Jan 2025 15:55:48 +0100 Subject: [PATCH 30/47] Updated text --- RefreshTools.py | 2 +- package.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/RefreshTools.py b/RefreshTools.py index 5caf2c7..97692e5 100644 --- a/RefreshTools.py +++ b/RefreshTools.py @@ -85,7 +85,7 @@ def refreshToolbars(doLoadAllWorkbenches=True): def refreshToolsAction(): from PySide import QtGui - print("Refresh list of tools") + print("Refresh cached results") fw = QtGui.QApplication.focusWidget() if fw is not None: fw.clearFocus() diff --git a/package.xml b/package.xml index c6e15d8..64bffdb 100644 --- a/package.xml +++ b/package.xml @@ -5,7 +5,7 @@ Adds a search bar widget for tools, document objects, and preferences - 1.3.1.2 + 1.3.2 2022-06-01 @@ -23,7 +23,7 @@ SearchBar - Resource/Icons/Tango-System-search.svg + Tango-System-search.svg ./ search widget From de05468d5e21810c404d2cbdac5b5759adc64e55 Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Sat, 11 Jan 2025 16:06:37 +0100 Subject: [PATCH 31/47] Update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d237003..b64a694 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ The search bar appears next to the [`What's this?`](https://wiki.freecad.org/Std ![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. From 0bbf54f0353222b06daf6145c8b347656b58c752 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 15:08:09 +0000 Subject: [PATCH 32/47] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- RefreshTools.py | 10 ++++++- SearchBox.py | 66 +++++++++++++++++++++++++++++++++++------------ SearchBoxLight.py | 8 ++++-- 3 files changed, 65 insertions(+), 19 deletions(-) diff --git a/RefreshTools.py b/RefreshTools.py index 97692e5..8c08dc8 100644 --- a/RefreshTools.py +++ b/RefreshTools.py @@ -14,7 +14,15 @@ def loadAllWorkbenches(): lbl.show() lst = FreeCADGui.listWorkbenches() for i, wb in enumerate(lst): - msg = translate("SearchBar", "Loading workbench ") + wb + " (" + str(i) + "/" + str(len(lst)) + ")" + msg = ( + translate("SearchBar", "Loading workbench ") + + wb + + " (" + + str(i) + + "/" + + str(len(lst)) + + ")" + ) print(msg) lbl.setText(msg) geo = lbl.geometry() diff --git a/SearchBox.py b/SearchBox.py index 492f1f3..254a5fa 100644 --- a/SearchBox.py +++ b/SearchBox.py @@ -91,7 +91,9 @@ class SearchBox(QLineEdit): 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 + self.maxVisibleRows = ( + maxVisibleRows # TODO: use this to compute the correct height + ) # Create proxy model 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. @@ -103,7 +105,9 @@ class SearchBox(QLineEdit): 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.setItemDelegate( + getItemDelegate() + ) # https://stackoverflow.com/a/65930408/324969 self.listView.setMouseTracking(True) # make the QListView non-editable self.listView.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) @@ -133,10 +137,18 @@ class SearchBox(QLineEdit): # Note: should probably use the eventFilter method instead... wdgctx = Qt.ShortcutContext.WidgetShortcut - 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) + 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. # QShortcut(QKeySequence.MoveToEndOfDocument, self, context = wdgctx).activated.connect(self.listEnd) @@ -144,13 +156,25 @@ class SearchBox(QLineEdit): # QShortcut(QKeySequence(Qt.Key.Key_End), self, context = wdgctx).activated.connect(self.listEnd) # QShortcut(QKeySequence('Home'), self, context = wdgctx).activated.connect(self.listStart) - 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) + 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 + ) - QShortcut(QKeySequence(Qt.Key.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 @@ -197,7 +221,9 @@ class SearchBox(QLineEdit): [ QStandardItem( genericToolIcon, - translate("SearchBar", "Please wait, loading results from cache…"), + translate( + "SearchBar", "Please wait, loading results from cache…" + ), ), QStandardItem("0"), QStandardItem("-1"), @@ -249,11 +275,17 @@ class SearchBox(QLineEdit): @staticmethod def proxyListPageDown(self): - self.movementKey(lambda current, nbRows: min(current + max(1, self.maxVisibleRows / 2), nbRows - 1)) + 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)) + self.movementKey( + lambda current, nbRows: max(current - max(1, self.maxVisibleRows / 2), 0) + ) @staticmethod def proxyListEnd(self): @@ -387,7 +419,9 @@ class SearchBox(QLineEdit): def getScreenPosition(widget): geo = widget.geometry() parent = widget.parent() - parentPos = getScreenPosition(parent) if parent is not None else QPoint(0, 0) + 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) diff --git a/SearchBoxLight.py b/SearchBoxLight.py index 81a1e6a..2c17826 100644 --- a/SearchBoxLight.py +++ b/SearchBoxLight.py @@ -6,7 +6,9 @@ from PySide import QtCore 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 @@ -25,7 +27,9 @@ class SearchBoxLight(QtGui.QLineEdit): 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.setFixedWidth( + 200 + ) # needed to avoid a change of width when the clear button appears/disappears def lazyInit(self): pass From a88b74beb5b5c38ebe7140873d2353ec3d2d16f4 Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Sun, 12 Jan 2025 20:09:05 +0100 Subject: [PATCH 33/47] When typing the second row is selected, so enter activates that command --- SearchBox.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/SearchBox.py b/SearchBox.py index 492f1f3..f9a0ae7 100644 --- a/SearchBox.py +++ b/SearchBox.py @@ -2,14 +2,14 @@ import FreeCAD as App import FreeCADGui as Gui import os -from PySide.QtCore import ( +from PySide6.QtCore import ( Qt, SIGNAL, QSize, QIdentityProxyModel, QPoint, ) -from PySide.QtWidgets import ( +from PySide6.QtWidgets import ( QTabWidget, QSlider, QSpinBox, @@ -28,7 +28,7 @@ from PySide.QtWidgets import ( QApplication, QListWidget, ) -from PySide.QtGui import ( +from PySide6.QtGui import ( QIcon, QPixmap, QColor, @@ -338,6 +338,8 @@ class SearchBox(QLineEdit): return group else: subitems = filterGroups(group["subitems"]) + # if len(subitems) == 0: + # self.index = 0 if len(subitems) > 0 or matches(group["text"]): return { "id": group["id"], @@ -373,9 +375,10 @@ class SearchBox(QLineEdit): 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: From 8d41c037e4f463aa64385ef0a9ccff4cf57b8493 Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Sun, 12 Jan 2025 20:17:08 +0100 Subject: [PATCH 34/47] fix import PySide6 --- SearchBox.py | 72 ++++++++++++++-------------------------------------- 1 file changed, 19 insertions(+), 53 deletions(-) diff --git a/SearchBox.py b/SearchBox.py index b9d07ad..c78c0d4 100644 --- a/SearchBox.py +++ b/SearchBox.py @@ -2,14 +2,14 @@ import FreeCAD as App import FreeCADGui as Gui import os -from PySide6.QtCore import ( +from PySide.QtCore import ( Qt, SIGNAL, QSize, QIdentityProxyModel, QPoint, ) -from PySide6.QtWidgets import ( +from PySide.QtWidgets import ( QTabWidget, QSlider, QSpinBox, @@ -28,7 +28,7 @@ from PySide6.QtWidgets import ( QApplication, QListWidget, ) -from PySide6.QtGui import ( +from PySide.QtGui import ( QIcon, QPixmap, QColor, @@ -91,9 +91,7 @@ class SearchBox(QLineEdit): 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 - ) + self.maxVisibleRows = maxVisibleRows # TODO: use this to compute the correct height # Create proxy model 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. @@ -105,9 +103,7 @@ class SearchBox(QLineEdit): 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.setItemDelegate(getItemDelegate()) # https://stackoverflow.com/a/65930408/324969 self.listView.setMouseTracking(True) # make the QListView non-editable self.listView.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) @@ -137,18 +133,10 @@ class SearchBox(QLineEdit): # Note: should probably use the eventFilter method instead... wdgctx = Qt.ShortcutContext.WidgetShortcut - 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) + 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. # QShortcut(QKeySequence.MoveToEndOfDocument, self, context = wdgctx).activated.connect(self.listEnd) @@ -156,25 +144,13 @@ class SearchBox(QLineEdit): # QShortcut(QKeySequence(Qt.Key.Key_End), self, context = wdgctx).activated.connect(self.listEnd) # QShortcut(QKeySequence('Home'), self, context = wdgctx).activated.connect(self.listStart) - 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 - ) + 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) - QShortcut( - QKeySequence(Qt.Key.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 @@ -221,9 +197,7 @@ class SearchBox(QLineEdit): [ QStandardItem( genericToolIcon, - translate( - "SearchBar", "Please wait, loading results from cache…" - ), + translate("SearchBar", "Please wait, loading results from cache…"), ), QStandardItem("0"), QStandardItem("-1"), @@ -275,17 +249,11 @@ class SearchBox(QLineEdit): @staticmethod def proxyListPageDown(self): - self.movementKey( - lambda current, nbRows: min( - current + max(1, self.maxVisibleRows / 2), nbRows - 1 - ) - ) + 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) - ) + self.movementKey(lambda current, nbRows: max(current - max(1, self.maxVisibleRows / 2), 0)) @staticmethod def proxyListEnd(self): @@ -422,9 +390,7 @@ class SearchBox(QLineEdit): def getScreenPosition(widget): geo = widget.geometry() parent = widget.parent() - parentPos = ( - getScreenPosition(parent) if parent is not None else QPoint(0, 0) - ) + 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) From d2a43e66b54c7505e263b86d50a27e6361c3eebe Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Sun, 12 Jan 2025 20:18:25 +0100 Subject: [PATCH 35/47] increased version --- package.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.xml b/package.xml index 64bffdb..e7bf5ea 100644 --- a/package.xml +++ b/package.xml @@ -5,7 +5,7 @@ Adds a search bar widget for tools, document objects, and preferences - 1.3.2 + 1.3.3 2022-06-01 From e8ebaefdc2f9b1ba36a4b6eb2ad16548b9393c66 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 12 Jan 2025 19:19:42 +0000 Subject: [PATCH 36/47] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- SearchBox.py | 66 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/SearchBox.py b/SearchBox.py index c78c0d4..9ac8bbf 100644 --- a/SearchBox.py +++ b/SearchBox.py @@ -91,7 +91,9 @@ class SearchBox(QLineEdit): 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 + self.maxVisibleRows = ( + maxVisibleRows # TODO: use this to compute the correct height + ) # Create proxy model 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. @@ -103,7 +105,9 @@ class SearchBox(QLineEdit): 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.setItemDelegate( + getItemDelegate() + ) # https://stackoverflow.com/a/65930408/324969 self.listView.setMouseTracking(True) # make the QListView non-editable self.listView.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) @@ -133,10 +137,18 @@ class SearchBox(QLineEdit): # Note: should probably use the eventFilter method instead... wdgctx = Qt.ShortcutContext.WidgetShortcut - 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) + 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. # QShortcut(QKeySequence.MoveToEndOfDocument, self, context = wdgctx).activated.connect(self.listEnd) @@ -144,13 +156,25 @@ class SearchBox(QLineEdit): # QShortcut(QKeySequence(Qt.Key.Key_End), self, context = wdgctx).activated.connect(self.listEnd) # QShortcut(QKeySequence('Home'), self, context = wdgctx).activated.connect(self.listStart) - 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) + 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 + ) - QShortcut(QKeySequence(Qt.Key.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 @@ -197,7 +221,9 @@ class SearchBox(QLineEdit): [ QStandardItem( genericToolIcon, - translate("SearchBar", "Please wait, loading results from cache…"), + translate( + "SearchBar", "Please wait, loading results from cache…" + ), ), QStandardItem("0"), QStandardItem("-1"), @@ -249,11 +275,17 @@ class SearchBox(QLineEdit): @staticmethod def proxyListPageDown(self): - self.movementKey(lambda current, nbRows: min(current + max(1, self.maxVisibleRows / 2), nbRows - 1)) + 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)) + self.movementKey( + lambda current, nbRows: max(current - max(1, self.maxVisibleRows / 2), 0) + ) @staticmethod def proxyListEnd(self): @@ -390,7 +422,9 @@ class SearchBox(QLineEdit): def getScreenPosition(widget): geo = widget.geometry() parent = widget.parent() - parentPos = getScreenPosition(parent) if parent is not None else QPoint(0, 0) + 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) From 1e0b949e3817498ef6847560a97fb8f4576243de Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Sun, 23 Feb 2025 15:50:02 +0100 Subject: [PATCH 37/47] Updated RefreshTools according RibbonUI updates --- InitGui.py | 126 +++++++---- RefreshTools.py | 43 ++-- SearchBox.py | 103 ++++----- StyleMapping.py | 338 +++++++++++++++++++++++++++++ translations/update_translation.sh | 2 + 5 files changed, 498 insertions(+), 114 deletions(-) create mode 100644 StyleMapping.py diff --git a/InitGui.py b/InitGui.py index bd5ca57..93dd1b2 100644 --- a/InitGui.py +++ b/InitGui.py @@ -1,71 +1,105 @@ import FreeCAD as App import FreeCADGui as Gui +from PySide6.QtWidgets import QWidgetAction, QToolBar, QMainWindow, QWidget, QDialog +from PySide6.QtGui import QCursor, QShortcut, QKeySequence, QAction +from PySide6.QtCore import Qt + # 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 +# class SearchBox: +mw = Gui.getMainWindow() + + def addToolSearchBox(): - import FreeCADGui - from PySide import QtGui - import SearchBoxLight - - # Define the translation - translate = App.Qt.translate - global wax, sea, tbr - mw = FreeCADGui.getMainWindow() + mw = Gui.getMainWindow() + import SearchBox + 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( - 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) - ##mbr.addWidget(sea) - # mbr.addAction(wax) + wax = SearchBox.SearchBoxFunction() 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() + # self.shortcut = QShortcut(QKeySequence("Alt+R"), self) + # self.shortcut.activated.connect(self.AddPointerBox) + # self.AddPointerBox() + print("shortcut toggled") + return + + +def AddPointerBox(): + import SearchBox + + print("shortcut toggled") + + Dialog = QDialog() + cursor = QCursor() + cursorPosition = cursor.pos() + + Dialog.geometry().setX(cursorPosition.x()) + Dialog.geometry().setY(cursorPosition.y()) + + Action = SearchBox.SearchBoxFunction() + Dialog.addAction(Action) + + Dialog.show() + return + + +# def SearchBoxFunction(): +# import SearchBoxLight + +# global wax, sea, tbr +# mw = Gui.getMainWindow() + +# if mw: +# if sea is None: +# sea = SearchBoxLight.SearchBoxLight( +# getItemGroups=lambda: __import__("GetItemGroups").getItemGroups(), +# getToolTip=lambda groupId, setParent: __import__("GetItemGroups").getToolTip(groupId, setParent), +# getItemDelegate=lambda: __import__("IndentedItemDelegate").IndentedItemDelegate(), +# ) +# sea.resultSelected.connect( +# lambda index, groupId: __import__("GetItemGroups").onResultSelected(index, groupId) +# ) + +# if wax is None: +# wax = 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 + addToolSearchBox() -import FreeCADGui - -FreeCADGui.getMainWindow().workbenchActivated.connect(addToolSearchBox) +Gui.getMainWindow().workbenchActivated.connect(addToolSearchBox) diff --git a/RefreshTools.py b/RefreshTools.py index 8c08dc8..c4b4068 100644 --- a/RefreshTools.py +++ b/RefreshTools.py @@ -1,41 +1,48 @@ import os import FreeCAD as App +import StyleMapping # Define the translation translate = App.Qt.translate def loadAllWorkbenches(): - from PySide import QtGui - import FreeCADGui + from PySide.QtGui import QLabel + from PySide.QtCore import Qt, SIGNAL, Signal, QObject, QThread, QSize + from PySide.QtGui import QIcon, QPixmap, QAction, QGuiApplication + import FreeCADGui as Gui + + 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.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) - activeWorkbench = FreeCADGui.activeWorkbench().name() - lbl = QtGui.QLabel(translate("SearchBar", "Loading workbench … (…/…)")) lbl.show() - lst = FreeCADGui.listWorkbenches() + lst = Gui.listWorkbenches() for i, wb in enumerate(lst): - msg = ( - translate("SearchBar", "Loading workbench ") - + wb - + " (" - + str(i) - + "/" - + str(len(lst)) - + ")" - ) + msg = translate("SearchBar", "Loading workbench ") + wb + " (" + str(i) + "/" + str(len(lst)) + ")" print(msg) lbl.setText(msg) geo = lbl.geometry() geo.setSize(lbl.sizeHint()) lbl.setGeometry(geo) lbl.repaint() - FreeCADGui.updateGui() # Probably slower with this, because it redraws the entire GUI with all tool buttons changed etc. but allows the label to actually be updated, and it looks nice and gives a quick overview of all the workbenches… + 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(): diff --git a/SearchBox.py b/SearchBox.py index 9ac8bbf..791453b 100644 --- a/SearchBox.py +++ b/SearchBox.py @@ -27,6 +27,7 @@ from PySide.QtWidgets import ( QVBoxLayout, QApplication, QListWidget, + QWidgetAction, ) from PySide.QtGui import ( QIcon, @@ -57,6 +58,42 @@ def easyToolTipWidget(html): return foo +def SearchBoxFunction(): + import SearchBoxLight + + global wax, sea, tbr + mw = Gui.getMainWindow() + + if mw: + if sea is None: + sea = SearchBoxLight.SearchBoxLight( + getItemGroups=lambda: __import__("GetItemGroups").getItemGroups(), + getToolTip=lambda groupId, setParent: __import__("GetItemGroups").getToolTip(groupId, setParent), + getItemDelegate=lambda: __import__("IndentedItemDelegate").IndentedItemDelegate(), + ) + sea.resultSelected.connect( + lambda index, groupId: __import__("GetItemGroups").onResultSelected(index, groupId) + ) + + if wax is None: + wax = 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 """ @@ -91,9 +128,7 @@ class SearchBox(QLineEdit): 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 - ) + self.maxVisibleRows = maxVisibleRows # TODO: use this to compute the correct height # Create proxy model 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. @@ -105,9 +140,7 @@ class SearchBox(QLineEdit): 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.setItemDelegate(getItemDelegate()) # https://stackoverflow.com/a/65930408/324969 self.listView.setMouseTracking(True) # make the QListView non-editable self.listView.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) @@ -137,18 +170,10 @@ class SearchBox(QLineEdit): # Note: should probably use the eventFilter method instead... wdgctx = Qt.ShortcutContext.WidgetShortcut - 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) + 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. # QShortcut(QKeySequence.MoveToEndOfDocument, self, context = wdgctx).activated.connect(self.listEnd) @@ -156,25 +181,13 @@ class SearchBox(QLineEdit): # QShortcut(QKeySequence(Qt.Key.Key_End), self, context = wdgctx).activated.connect(self.listEnd) # QShortcut(QKeySequence('Home'), self, context = wdgctx).activated.connect(self.listStart) - 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 - ) + 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) - QShortcut( - QKeySequence(Qt.Key.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 @@ -221,9 +234,7 @@ class SearchBox(QLineEdit): [ QStandardItem( genericToolIcon, - translate( - "SearchBar", "Please wait, loading results from cache…" - ), + translate("SearchBar", "Please wait, loading results from cache…"), ), QStandardItem("0"), QStandardItem("-1"), @@ -275,17 +286,11 @@ class SearchBox(QLineEdit): @staticmethod def proxyListPageDown(self): - self.movementKey( - lambda current, nbRows: min( - current + max(1, self.maxVisibleRows / 2), nbRows - 1 - ) - ) + 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) - ) + self.movementKey(lambda current, nbRows: max(current - max(1, self.maxVisibleRows / 2), 0)) @staticmethod def proxyListEnd(self): @@ -422,9 +427,7 @@ class SearchBox(QLineEdit): def getScreenPosition(widget): geo = widget.geometry() parent = widget.parent() - parentPos = ( - getScreenPosition(parent) if parent is not None else QPoint(0, 0) - ) + 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) diff --git a/StyleMapping.py b/StyleMapping.py new file mode 100644 index 0000000..e351448 --- /dev/null +++ b/StyleMapping.py @@ -0,0 +1,338 @@ +# ************************************************************************* +# * * +# * 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 ReturnStyleSheet(control, radius="2px", padding_right="0px", padding_bottom="0px", width="16px"): + """ + Enter one of the names below: + + control (string): + toolbutton, + toolbuttonLarge, + applicationbutton, + """ + StyleSheet = "" + try: + BorderColor = ReturnStyleItem("Border_Color") + BackgroundColor = ReturnStyleItem("Background_Color") + ApplicationButton = ReturnStyleItem("ApplicationButton_Background") + HoverColor = ReturnStyleItem("Background_Color_Hover") + FontColor = ReturnStyleItem("FontColor") + + AppColor_1 = ApplicationButton + AppColor_2 = ApplicationButton + AppColor_3 = ApplicationButton + AppBorder_1 = BorderColor + AppBorder_2 = BorderColor + if BackgroundColor is not None and BorderColor is not None: + if control.lower() == "toolbutton": + if Parameters_Ribbon.BORDER_TRANSPARANT is True: + BorderColor = BackgroundColor + StyleSheet = ( + """QLayout {spacing: 0px}""" + + """QToolButton, QTextEdit { + margin: 0px; + padding: 0px; + color: """ + + FontColor + + """;background: """ + + BackgroundColor + + """;padding-bottom: """ + + padding_bottom + + """;padding-right: """ + + padding_right + + """;padding-left: 0px; + spacing: 0px;}""" + + """QToolButton::menu-arrow { + subcontrol-origin: padding; + subcontrol-position: center right; + }""" + + """QToolButton::menu-button { + margin: 0px; + padding: 0px; + width: """ + + width + + """; + border-radius: """ + + radius + + """px;""" + + """padding: 0px; + subcontrol-origin: padding; + subcontrol-position: center right; + }""" + + """QToolButton:hover, QTextEdit:hover { + margin: 0px 0px 0px 0px; + padding: 0px; + border: none; + background: """ + + HoverColor + + """;padding-bottom: """ + + padding_bottom + + """;padding-right: """ + + padding_right + + """;border: 0.5px solid""" + + BorderColor + + """;}""" + ) + return StyleSheet + if control.lower() == "applicationbutton": + StyleSheet = ( + """QToolButton { + border-radius : """ + + radius + + """;padding-right: """ + + padding_right + + """;background-color: """ + + AppColor_1 + + """;border: 0.5px solid""" + + BorderColor + + """;}""" + + """QToolButton:hover { """ + + """border: 2px solid""" + + BorderColor + + """;border-radius : """ + + radius + + """;}""" + ) + + return StyleSheet + except Exception as e: + print(e) + return StyleSheet + + +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(), + }, + } +} diff --git a/translations/update_translation.sh b/translations/update_translation.sh index 1422655..77e7d36 100644 --- a/translations/update_translation.sh +++ b/translations/update_translation.sh @@ -114,8 +114,10 @@ help() { # 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" From f7b0eb0a1be07a0c1984eba2f06874a775b05640 Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Sun, 23 Feb 2025 15:50:41 +0100 Subject: [PATCH 38/47] Update version --- package.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.xml b/package.xml index e7bf5ea..ff3b094 100644 --- a/package.xml +++ b/package.xml @@ -5,7 +5,7 @@ Adds a search bar widget for tools, document objects, and preferences - 1.3.3 + 1.4.0 2022-06-01 From 3468e41185bf341a44ce4fe4118ec393ad592888 Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Sun, 23 Feb 2025 15:55:23 +0100 Subject: [PATCH 39/47] Removed unused code --- SearchBox.py | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/SearchBox.py b/SearchBox.py index 791453b..76b856a 100644 --- a/SearchBox.py +++ b/SearchBox.py @@ -58,42 +58,6 @@ def easyToolTipWidget(html): return foo -def SearchBoxFunction(): - import SearchBoxLight - - global wax, sea, tbr - mw = Gui.getMainWindow() - - if mw: - if sea is None: - sea = SearchBoxLight.SearchBoxLight( - getItemGroups=lambda: __import__("GetItemGroups").getItemGroups(), - getToolTip=lambda groupId, setParent: __import__("GetItemGroups").getToolTip(groupId, setParent), - getItemDelegate=lambda: __import__("IndentedItemDelegate").IndentedItemDelegate(), - ) - sea.resultSelected.connect( - lambda index, groupId: __import__("GetItemGroups").onResultSelected(index, groupId) - ) - - if wax is None: - wax = 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 """ From 1859382302b51ae71f247c9fbdf8a46ce46a854d Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Sun, 23 Feb 2025 15:59:52 +0100 Subject: [PATCH 40/47] Uncomment code --- InitGui.py | 60 +++++++++++++++++++++++++++--------------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/InitGui.py b/InitGui.py index 93dd1b2..285ac87 100644 --- a/InitGui.py +++ b/InitGui.py @@ -65,40 +65,40 @@ def AddPointerBox(): return -# def SearchBoxFunction(): -# import SearchBoxLight +def SearchBoxFunction(): + import SearchBoxLight -# global wax, sea, tbr -# mw = Gui.getMainWindow() + global wax, sea, tbr + mw = Gui.getMainWindow() -# if mw: -# if sea is None: -# sea = SearchBoxLight.SearchBoxLight( -# getItemGroups=lambda: __import__("GetItemGroups").getItemGroups(), -# getToolTip=lambda groupId, setParent: __import__("GetItemGroups").getToolTip(groupId, setParent), -# getItemDelegate=lambda: __import__("IndentedItemDelegate").IndentedItemDelegate(), -# ) -# sea.resultSelected.connect( -# lambda index, groupId: __import__("GetItemGroups").onResultSelected(index, groupId) -# ) + if 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", -# ) -# ) + 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 + sea.setWhatsThis( + translate( + "SearchBar", + "Use this search bar to find tools, document objects, preferences and more", + ) + ) + wax.setDefaultWidget(sea) + return wax addToolSearchBox() From fd0500382cda4458e0e89e7062d55244802d0e2d Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Sun, 23 Feb 2025 16:13:41 +0100 Subject: [PATCH 41/47] furter corrections --- InitGui.py | 43 ++------------------- RefreshTools.py | 13 ++++--- SearchBox.py | 41 ++++++++++++++++++++ StyleMapping.py | 99 ------------------------------------------------- 4 files changed, 52 insertions(+), 144 deletions(-) diff --git a/InitGui.py b/InitGui.py index 285ac87..ed3ede9 100644 --- a/InitGui.py +++ b/InitGui.py @@ -1,9 +1,9 @@ import FreeCAD as App import FreeCADGui as Gui -from PySide6.QtWidgets import QWidgetAction, QToolBar, QMainWindow, QWidget, QDialog -from PySide6.QtGui import QCursor, QShortcut, QKeySequence, QAction -from PySide6.QtCore import Qt +from PySide.QtWidgets import QWidgetAction, QToolBar, QMainWindow, QWidget, QDialog +from PySide.QtGui import QCursor, QShortcut, QKeySequence, QAction +from PySide.QtCore import Qt # Avoid garbage collection by storing the action in a global variable wax = None @@ -26,6 +26,7 @@ def addToolSearchBox(): global wax, sea, tbr mw = Gui.getMainWindow() import SearchBox + from PySide.QtWidgets import QToolBar if mw: if sea is None: @@ -65,41 +66,5 @@ def AddPointerBox(): return -def SearchBoxFunction(): - import SearchBoxLight - - global wax, sea, tbr - mw = Gui.getMainWindow() - - if mw: - if sea is None: - sea = SearchBoxLight.SearchBoxLight( - getItemGroups=lambda: __import__("GetItemGroups").getItemGroups(), - getToolTip=lambda groupId, setParent: __import__("GetItemGroups").getToolTip(groupId, setParent), - getItemDelegate=lambda: __import__("IndentedItemDelegate").IndentedItemDelegate(), - ) - sea.resultSelected.connect( - lambda index, groupId: __import__("GetItemGroups").onResultSelected(index, groupId) - ) - - if wax is None: - wax = 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 - - addToolSearchBox() Gui.getMainWindow().workbenchActivated.connect(addToolSearchBox) diff --git a/RefreshTools.py b/RefreshTools.py index c4b4068..e1ebbb2 100644 --- a/RefreshTools.py +++ b/RefreshTools.py @@ -1,5 +1,6 @@ import os import FreeCAD as App +import FreeCADGui as Gui import StyleMapping # Define the translation @@ -7,10 +8,10 @@ translate = App.Qt.translate def loadAllWorkbenches(): + 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 - import FreeCADGui as Gui activeWorkbench = Gui.activeWorkbench().name() lbl = QLabel(translate("SearchBar", "Loading workbench … (…/…)")) @@ -19,11 +20,11 @@ def loadAllWorkbenches(): # Get the stylesheet from the main window and use it for this form lbl.setStyleSheet("background-color: " + StyleMapping.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) + # # 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 = Gui.listWorkbenches() diff --git a/SearchBox.py b/SearchBox.py index 76b856a..3ba2b9b 100644 --- a/SearchBox.py +++ b/SearchBox.py @@ -49,6 +49,11 @@ globalIgnoreFocusOut = False # 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 = QTextEdit() @@ -58,6 +63,42 @@ def easyToolTipWidget(html): return foo +def SearchBoxFunction(): + import SearchBoxLight + + global wax, sea, tbr + mw = Gui.getMainWindow() + + if mw: + if sea is None: + sea = SearchBoxLight.SearchBoxLight( + getItemGroups=lambda: __import__("GetItemGroups").getItemGroups(), + getToolTip=lambda groupId, setParent: __import__("GetItemGroups").getToolTip(groupId, setParent), + getItemDelegate=lambda: __import__("IndentedItemDelegate").IndentedItemDelegate(), + ) + sea.resultSelected.connect( + lambda index, groupId: __import__("GetItemGroups").onResultSelected(index, groupId) + ) + + if wax is None: + wax = 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 """ diff --git a/StyleMapping.py b/StyleMapping.py index e351448..4417b71 100644 --- a/StyleMapping.py +++ b/StyleMapping.py @@ -91,105 +91,6 @@ def ReturnStyleItem(ControlName, ShowCustomIcon=False, IgnoreOverlay=False): return None -def ReturnStyleSheet(control, radius="2px", padding_right="0px", padding_bottom="0px", width="16px"): - """ - Enter one of the names below: - - control (string): - toolbutton, - toolbuttonLarge, - applicationbutton, - """ - StyleSheet = "" - try: - BorderColor = ReturnStyleItem("Border_Color") - BackgroundColor = ReturnStyleItem("Background_Color") - ApplicationButton = ReturnStyleItem("ApplicationButton_Background") - HoverColor = ReturnStyleItem("Background_Color_Hover") - FontColor = ReturnStyleItem("FontColor") - - AppColor_1 = ApplicationButton - AppColor_2 = ApplicationButton - AppColor_3 = ApplicationButton - AppBorder_1 = BorderColor - AppBorder_2 = BorderColor - if BackgroundColor is not None and BorderColor is not None: - if control.lower() == "toolbutton": - if Parameters_Ribbon.BORDER_TRANSPARANT is True: - BorderColor = BackgroundColor - StyleSheet = ( - """QLayout {spacing: 0px}""" - + """QToolButton, QTextEdit { - margin: 0px; - padding: 0px; - color: """ - + FontColor - + """;background: """ - + BackgroundColor - + """;padding-bottom: """ - + padding_bottom - + """;padding-right: """ - + padding_right - + """;padding-left: 0px; - spacing: 0px;}""" - + """QToolButton::menu-arrow { - subcontrol-origin: padding; - subcontrol-position: center right; - }""" - + """QToolButton::menu-button { - margin: 0px; - padding: 0px; - width: """ - + width - + """; - border-radius: """ - + radius - + """px;""" - + """padding: 0px; - subcontrol-origin: padding; - subcontrol-position: center right; - }""" - + """QToolButton:hover, QTextEdit:hover { - margin: 0px 0px 0px 0px; - padding: 0px; - border: none; - background: """ - + HoverColor - + """;padding-bottom: """ - + padding_bottom - + """;padding-right: """ - + padding_right - + """;border: 0.5px solid""" - + BorderColor - + """;}""" - ) - return StyleSheet - if control.lower() == "applicationbutton": - StyleSheet = ( - """QToolButton { - border-radius : """ - + radius - + """;padding-right: """ - + padding_right - + """;background-color: """ - + AppColor_1 - + """;border: 0.5px solid""" - + BorderColor - + """;}""" - + """QToolButton:hover { """ - + """border: 2px solid""" - + BorderColor - + """;border-radius : """ - + radius - + """;}""" - ) - - return StyleSheet - except Exception as e: - print(e) - return StyleSheet - - def ReturnColor(ColorType="Background_Color"): mw: QMainWindow = Gui.getMainWindow() palette = mw.style().standardPalette() From 84c8ee716002228a5e83086687d43c22ec8ea607 Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Sun, 23 Feb 2025 16:19:46 +0100 Subject: [PATCH 42/47] Changed name for StyleMapping --- RefreshTools.py | 4 ++-- StyleMapping.py => StyleMapping_SearchBar.py | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename StyleMapping.py => StyleMapping_SearchBar.py (100%) diff --git a/RefreshTools.py b/RefreshTools.py index e1ebbb2..2d8cac5 100644 --- a/RefreshTools.py +++ b/RefreshTools.py @@ -1,7 +1,7 @@ import os import FreeCAD as App import FreeCADGui as Gui -import StyleMapping +import StyleMapping_SearchBar # Define the translation translate = App.Qt.translate @@ -18,7 +18,7 @@ def loadAllWorkbenches(): lbl.setWindowFlags(Qt.WindowType.WindowStaysOnTopHint) # Get the stylesheet from the main window and use it for this form - lbl.setStyleSheet("background-color: " + StyleMapping.ReturnStyleItem("Background_Color") + ";") + lbl.setStyleSheet("background-color: " + StyleMapping_SearchBar.ReturnStyleItem("Background_Color") + ";") # # Get the main window from FreeCAD # mw = Gui.getMainWindow() diff --git a/StyleMapping.py b/StyleMapping_SearchBar.py similarity index 100% rename from StyleMapping.py rename to StyleMapping_SearchBar.py From 349c8c70f5d0c123a2813cc150e0edea9fd7c389 Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Sun, 23 Feb 2025 16:45:57 +0100 Subject: [PATCH 43/47] updated messagebox --- RefreshTools.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/RefreshTools.py b/RefreshTools.py index 2d8cac5..d4fe6e8 100644 --- a/RefreshTools.py +++ b/RefreshTools.py @@ -99,23 +99,26 @@ def refreshToolbars(doLoadAllWorkbenches=True): def refreshToolsAction(): - from PySide import QtGui + from PySide.QtWidgets import QApplication, QMessageBox + from PySide.QtCore import Qt print("Refresh cached results") - fw = QtGui.QApplication.focusWidget() - if fw is not None: - fw.clearFocus() - reply = QtGui.QMessageBox.question( + # fw = QApplication.focusWidget() + # if fw is not None: + # fw.clearFocus() + msgBox = QMessageBox() + msgBox.setWindowFlags(Qt.WindowType.WindowStaysOnTopHint) + reply = msgBox.question( None, 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.""", ), - QtGui.QMessageBox.Yes, - QtGui.QMessageBox.No, + QMessageBox.Yes, + QMessageBox.No, ) - if reply == QtGui.QMessageBox.Yes: + if reply == QMessageBox.Yes: refreshToolbars() else: print("cancelled") From d9b07e6b4c656c361b00ab684b0c2c3e5a6bc7e0 Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Sun, 23 Feb 2025 17:10:42 +0100 Subject: [PATCH 44/47] Fixed message falling behind FreeCAD --- RefreshTools.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/RefreshTools.py b/RefreshTools.py index d4fe6e8..38dcd78 100644 --- a/RefreshTools.py +++ b/RefreshTools.py @@ -103,13 +103,12 @@ def refreshToolsAction(): from PySide.QtCore import Qt print("Refresh cached results") - # fw = QApplication.focusWidget() - # if fw is not None: - # fw.clearFocus() msgBox = QMessageBox() msgBox.setWindowFlags(Qt.WindowType.WindowStaysOnTopHint) + # Get the main window from FreeCAD + mw = Gui.getMainWindow() reply = msgBox.question( - None, + mw, translate("SearchBar", "Load all workbenches?"), translate( "SearchBar", From b49c45ab941e1f7948afc7d2b77a10e8fe232882 Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Sun, 23 Feb 2025 17:20:30 +0100 Subject: [PATCH 45/47] Updated counter to +1 --- RefreshTools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RefreshTools.py b/RefreshTools.py index 38dcd78..acd8d15 100644 --- a/RefreshTools.py +++ b/RefreshTools.py @@ -29,7 +29,7 @@ def loadAllWorkbenches(): lbl.show() lst = Gui.listWorkbenches() for i, wb in enumerate(lst): - msg = translate("SearchBar", "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() From 948fd924ca1400f9278045481fc854a5ee829dab Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Sun, 23 Feb 2025 18:03:29 +0100 Subject: [PATCH 46/47] Cleanup code --- InitGui.py | 39 ++++----------------------------------- SearchBox.py | 3 +-- 2 files changed, 5 insertions(+), 37 deletions(-) diff --git a/InitGui.py b/InitGui.py index ed3ede9..999a972 100644 --- a/InitGui.py +++ b/InitGui.py @@ -1,10 +1,6 @@ import FreeCAD as App import FreeCADGui as Gui -from PySide.QtWidgets import QWidgetAction, QToolBar, QMainWindow, QWidget, QDialog -from PySide.QtGui import QCursor, QShortcut, QKeySequence, QAction -from PySide.QtCore import Qt - # Avoid garbage collection by storing the action in a global variable wax = None sea = None @@ -18,19 +14,16 @@ def QT_TRANSLATE_NOOP(context, text): return text -# class SearchBox: -mw = Gui.getMainWindow() - - def addToolSearchBox(): global wax, sea, tbr mw = Gui.getMainWindow() import SearchBox - from PySide.QtWidgets import QToolBar + from PySide6.QtWidgets import QToolBar + from PySide6.QtGui import QShortcut, QKeySequence if mw: if sea is None: - wax = SearchBox.SearchBoxFunction() + wax = SearchBox.SearchBoxFunction(mw) if tbr is None: tbr = QToolBar("SearchBar") # QtGui.QDockWidget() # Include FreeCAD in the name so that one can find windows labeled with @@ -39,31 +32,7 @@ def addToolSearchBox(): tbr.addAction(wax) mw.addToolBar(tbr) tbr.show() - - # self.shortcut = QShortcut(QKeySequence("Alt+R"), self) - # self.shortcut.activated.connect(self.AddPointerBox) - # self.AddPointerBox() - print("shortcut toggled") - return - - -def AddPointerBox(): - import SearchBox - - print("shortcut toggled") - - Dialog = QDialog() - cursor = QCursor() - cursorPosition = cursor.pos() - - Dialog.geometry().setX(cursorPosition.x()) - Dialog.geometry().setY(cursorPosition.y()) - - Action = SearchBox.SearchBoxFunction() - Dialog.addAction(Action) - - Dialog.show() - return + return addToolSearchBox() diff --git a/SearchBox.py b/SearchBox.py index 3ba2b9b..e3160a0 100644 --- a/SearchBox.py +++ b/SearchBox.py @@ -63,11 +63,10 @@ def easyToolTipWidget(html): return foo -def SearchBoxFunction(): +def SearchBoxFunction(mw): import SearchBoxLight global wax, sea, tbr - mw = Gui.getMainWindow() if mw: if sea is None: From 9d2f127e517f375bde200e2a9bca42c39f2688a4 Mon Sep 17 00:00:00 2001 From: Paul Ebbers Date: Wed, 26 Feb 2025 20:20:14 +0100 Subject: [PATCH 47/47] Fix import issue --- InitGui.py | 4 ++-- package.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/InitGui.py b/InitGui.py index 999a972..d5517b1 100644 --- a/InitGui.py +++ b/InitGui.py @@ -18,8 +18,8 @@ def addToolSearchBox(): global wax, sea, tbr mw = Gui.getMainWindow() import SearchBox - from PySide6.QtWidgets import QToolBar - from PySide6.QtGui import QShortcut, QKeySequence + from PySide.QtWidgets import QToolBar + from PySide.QtGui import QShortcut, QKeySequence if mw: if sea is None: diff --git a/package.xml b/package.xml index ff3b094..f9d0d5e 100644 --- a/package.xml +++ b/package.xml @@ -5,7 +5,7 @@ Adds a search bar widget for tools, document objects, and preferences - 1.4.0 + 1.4.1 2022-06-01