diff --git a/InitGui.py b/InitGui.py index bd5ca57..999a972 100644 --- a/InitGui.py +++ b/InitGui.py @@ -6,66 +6,34 @@ wax = None sea = None tbr = None +# Define the translation +translate = App.Qt.translate + def QT_TRANSLATE_NOOP(context, text): return text def addToolSearchBox(): - import FreeCADGui - from PySide import QtGui - import SearchBoxLight - - # Define the translation - translate = App.Qt.translate - global wax, sea, tbr - mw = FreeCADGui.getMainWindow() + mw = Gui.getMainWindow() + import SearchBox + from PySide6.QtWidgets import QToolBar + from PySide6.QtGui import QShortcut, QKeySequence + if mw: if sea is None: - sea = SearchBoxLight.SearchBoxLight( - getItemGroups=lambda: __import__("GetItemGroups").getItemGroups(), - getToolTip=lambda groupId, setParent: __import__( - "GetItemGroups" - ).getToolTip(groupId, setParent), - getItemDelegate=lambda: __import__( - "IndentedItemDelegate" - ).IndentedItemDelegate(), - ) - sea.resultSelected.connect( - lambda index, groupId: __import__("GetItemGroups").onResultSelected( - index, groupId - ) - ) - - if wax is None: - wax = QtGui.QWidgetAction(None) - wax.setWhatsThis( - 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(mw) if tbr is None: - tbr = QtGui.QToolBar("SearchBar") # QtGui.QDockWidget() - # Include FreeCAD in the name so that one can find windows labeled with FreeCAD easily in window managers which allow search through the list of open windows. + tbr = QToolBar("SearchBar") # QtGui.QDockWidget() + # Include FreeCAD in the name so that one can find windows labeled with + # FreeCAD easily in window managers which allow search through the list of open windows. tbr.setObjectName("SearchBar") tbr.addAction(wax) mw.addToolBar(tbr) tbr.show() + return addToolSearchBox() -import FreeCADGui - -FreeCADGui.getMainWindow().workbenchActivated.connect(addToolSearchBox) +Gui.getMainWindow().workbenchActivated.connect(addToolSearchBox) diff --git a/RefreshTools.py b/RefreshTools.py index 8c08dc8..acd8d15 100644 --- a/RefreshTools.py +++ b/RefreshTools.py @@ -1,41 +1,49 @@ import os import FreeCAD as App +import FreeCADGui as Gui +import StyleMapping_SearchBar # Define the translation translate = App.Qt.translate def loadAllWorkbenches(): - from PySide import QtGui - import FreeCADGui + import FreeCADGui as Gui + from PySide.QtGui import QLabel + from PySide.QtCore import Qt, SIGNAL, Signal, QObject, QThread, QSize + from PySide.QtGui import QIcon, QPixmap, QAction, QGuiApplication + + activeWorkbench = Gui.activeWorkbench().name() + lbl = QLabel(translate("SearchBar", "Loading workbench … (…/…)")) + lbl.setWindowFlags(Qt.WindowType.WindowStaysOnTopHint) + + # Get the stylesheet from the main window and use it for this form + lbl.setStyleSheet("background-color: " + StyleMapping_SearchBar.ReturnStyleItem("Background_Color") + ";") + + # # Get the main window from FreeCAD + # mw = Gui.getMainWindow() + # # Center the widget + # cp = QGuiApplication.screenAt(mw.pos()).geometry().center() + # lbl.move(cp) - 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 + 1) + "/" + str(len(lst)) + ")" print(msg) lbl.setText(msg) geo = lbl.geometry() geo.setSize(lbl.sizeHint()) lbl.setGeometry(geo) lbl.repaint() - FreeCADGui.updateGui() # Probably slower with this, because it redraws the entire GUI with all tool buttons changed etc. but allows the label to actually be updated, and it looks nice and gives a quick overview of all the workbenches… + Gui.updateGui() # Probably slower with this, because it redraws the entire GUI with all tool buttons changed etc. but allows the label to actually be updated, and it looks nice and gives a quick overview of all the workbenches… try: - FreeCADGui.activateWorkbench(wb) - except: + Gui.activateWorkbench(wb) + except Exception: pass lbl.hide() - FreeCADGui.activateWorkbench(activeWorkbench) + Gui.activateWorkbench(activeWorkbench) + return def cachePath(): @@ -91,23 +99,25 @@ 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( - None, + msgBox = QMessageBox() + msgBox.setWindowFlags(Qt.WindowType.WindowStaysOnTopHint) + # Get the main window from FreeCAD + mw = Gui.getMainWindow() + reply = msgBox.question( + mw, translate("SearchBar", "Load all workbenches?"), translate( "SearchBar", """Load all workbenches? This can cause FreeCAD to become unstable, and this "reload tools" feature contained a bug that crashed freecad systematically, so please make sure you save your work before. It\'s a good idea to restart FreeCAD after this operation.""", ), - QtGui.QMessageBox.Yes, - QtGui.QMessageBox.No, + QMessageBox.Yes, + QMessageBox.No, ) - if reply == QtGui.QMessageBox.Yes: + if reply == QMessageBox.Yes: refreshToolbars() else: print("cancelled") diff --git a/SearchBox.py b/SearchBox.py index 9ac8bbf..e3160a0 100644 --- a/SearchBox.py +++ b/SearchBox.py @@ -27,6 +27,7 @@ from PySide.QtWidgets import ( QVBoxLayout, QApplication, QListWidget, + QWidgetAction, ) from PySide.QtGui import ( QIcon, @@ -48,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() @@ -57,6 +63,41 @@ def easyToolTipWidget(html): return foo +def SearchBoxFunction(mw): + import SearchBoxLight + + global wax, sea, tbr + + if mw: + if sea is None: + sea = SearchBoxLight.SearchBoxLight( + getItemGroups=lambda: __import__("GetItemGroups").getItemGroups(), + getToolTip=lambda groupId, setParent: __import__("GetItemGroups").getToolTip(groupId, setParent), + getItemDelegate=lambda: __import__("IndentedItemDelegate").IndentedItemDelegate(), + ) + sea.resultSelected.connect( + lambda index, groupId: __import__("GetItemGroups").onResultSelected(index, groupId) + ) + + if wax is None: + wax = QWidgetAction(None) + wax.setWhatsThis( + translate( + "SearchBar", + "Use this search bar to find tools, document objects, preferences and more", + ) + ) + + sea.setWhatsThis( + translate( + "SearchBar", + "Use this search bar to find tools, document objects, preferences and more", + ) + ) + wax.setDefaultWidget(sea) + return wax + + class SearchBox(QLineEdit): # The following block of code is present in the lightweight proxy SearchBoxLight """ @@ -91,9 +132,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 +144,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 +174,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 +185,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 +238,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 +290,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 +431,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_SearchBar.py b/StyleMapping_SearchBar.py new file mode 100644 index 0000000..4417b71 --- /dev/null +++ b/StyleMapping_SearchBar.py @@ -0,0 +1,239 @@ +# ************************************************************************* +# * * +# * Copyright (c) 2019-2024 Paul Ebbers * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 3 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# ************************************************************************* +import FreeCAD as App +import FreeCADGui as Gui +import os +from PySide.QtGui import QIcon, QPixmap, QAction +from PySide.QtWidgets import ( + QListWidgetItem, + QTableWidgetItem, + QListWidget, + QTableWidget, + QToolBar, + QToolButton, + QComboBox, + QPushButton, + QMenu, + QWidget, + QMainWindow, +) +from PySide.QtCore import Qt, SIGNAL, Signal, QObject, QThread +import sys +import json +from datetime import datetime +import shutil +import Standard_Functions_RIbbon as StandardFunctions +import Parameters_Ribbon +import webbrowser +import time + +# Get the resources +pathIcons = Parameters_Ribbon.ICON_LOCATION +pathStylSheets = Parameters_Ribbon.STYLESHEET_LOCATION +pathUI = Parameters_Ribbon.UI_LOCATION +pathBackup = Parameters_Ribbon.BACKUP_LOCATION +sys.path.append(pathIcons) +sys.path.append(pathStylSheets) +sys.path.append(pathUI) +sys.path.append(pathBackup) + + +def ReturnStyleItem(ControlName, ShowCustomIcon=False, IgnoreOverlay=False): + """ + Enter one of the names below: + + ControlName (string): + "Background_Color" returns string, + "Border_Color" returns string, + "FontColor" returns string, + "FontColor" returns string, + """ + # define a result holder and a dict for the StyleMapping file + result = "none" + + # Get the current stylesheet for FreeCAD + FreeCAD_preferences = App.ParamGet("User parameter:BaseApp/Preferences/MainWindow") + currentStyleSheet = FreeCAD_preferences.GetString("StyleSheet") + IsInList = False + for key, value in StyleMapping_default["Stylesheets"].items(): + if key == currentStyleSheet: + IsInList = True + break + if IsInList is False: + currentStyleSheet = "none" + + try: + result = StyleMapping_default["Stylesheets"][currentStyleSheet][ControlName] + if result == "" or result is None: + result = StyleMapping_default["Stylesheets"][""][ControlName] + return result + except Exception as e: + print(e) + return None + + +def ReturnColor(ColorType="Background_Color"): + mw: QMainWindow = Gui.getMainWindow() + palette = mw.style().standardPalette() + # Get the color + Color = palette.base().color().toTuple() # RGBA tupple + if ColorType == "Border_Color": + Color = palette.buttonText().color().toTuple() + if ColorType == "Background_Color_Hover": + Color = palette.highlight().color().toTuple() + + HexColor = StandardFunctions.ColorConvertor(Color, Color[3] / 255, True, False) + + return HexColor + + +def ReturnFontColor(): + fontColor = "#000000" + IsDarkTheme = DarkMode() + + if IsDarkTheme is True: + fontColor = "#ffffff" + + return fontColor + + +def DarkMode(): + import xml.etree.ElementTree as ET + import os + + # Define the standard result + IsDarkTheme = False + + # Get the current stylesheet for FreeCAD + FreeCAD_preferences = App.ParamGet("User parameter:BaseApp/Preferences/MainWindow") + currentStyleSheet = FreeCAD_preferences.GetString("StyleSheet") + + path = os.path.dirname(__file__) + # Get the folder with add-ons + for i in range(2): + # Starting point + path = os.path.dirname(path) + + # Go through the sub-folders + for root, dirs, files in os.walk(path): + for name in dirs: + # if the current stylesheet matches a sub directory, try to geth the pacakgexml + if currentStyleSheet.replace(".qss", "").lower() in name.lower(): + try: + packageXML = os.path.join(path, name, "package.xml") + + # Get the tree and root of the xml file + tree = ET.parse(packageXML) + treeRoot = tree.getroot() + + # Get all the tag elements + elements = [] + namespaces = {"i": "https://wiki.freecad.org/Package_Metadata"} + elements = treeRoot.findall(".//i:content/i:preferencepack/i:tag", namespaces) + + # go throug all tags. If 'dark' in the element text, this is a dark theme + for element in elements: + if "dark" in element.text.lower(): + IsDarkTheme = True + break + except Exception: + continue + + return IsDarkTheme + + +StyleMapping_default = { + "Stylesheets": { + "": { + "Background_Color": "#f0f0f0", + "Background_Color_Hover": "#ced4da", + "Border_Color": "#646464", + "FontColor": ReturnFontColor(), + }, + "none": { + "Background_Color": "none", + "Background_Color_Hover": "#48a0f8", + "Border_Color": ReturnColor("Border_Color"), + "FontColor": ReturnFontColor(), + }, + "FreeCAD Dark.qss": { + "Background_Color": "#333333", + "Background_Color_Hover": "#48a0f8", + "Border_Color": "#ffffff", + "FontColor": "#ffffff", + }, + "FreeCAD Light.qss": { + "Background_Color": "#f0f0f0", + "Background_Color_Hover": "#48a0f8", + "Border_Color": "#646464", + "FontColor": "#000000", + }, + "OpenLight.qss": { + "Background_Color": "#dee2e6", + "Background_Color_Hover": "#a5d8ff", + "Border_Color": "#1c7ed6", + "FontColor": "#000000", + }, + "OpenDark.qss": { + "Background_Color": "#212529", + "Background_Color_Hover": "#1f364d", + "Border_Color": "#264b69", + "FontColor": "#ffffff", + }, + "Behave-dark.qss": { + "Background_Color": "#232932", + "Background_Color_Hover": "#557bb6", + "Border_Color": "#3a7400", + "FontColor": ReturnFontColor(), + }, + "ProDark.qss": { + "Background_Color": "#333333", + "Background_Color_Hover": "#557bb6", + "Border_Color": "#adc5ed", + "FontColor": ReturnFontColor(), + }, + "Darker.qss": { + "Background_Color": "#444444", + "Background_Color_Hover": "#4aa5ff", + "Border_Color": "#696968", + "FontColor": ReturnFontColor(), + }, + "Light-modern.qss": { + "Background_Color": "#f0f0f0", + "Background_Color_Hover": "#4aa5ff", + "Border_Color": "#646464", + "FontColor": ReturnFontColor(), + }, + "Dark-modern.qss": { + "Background_Color": "#2b2b2b", + "Background_Color_Hover": "#4aa5ff", + "Border_Color": "#ffffff", + "FontColor": ReturnFontColor(), + }, + "Dark-contrast.qss": { + "Background_Color": "#444444", + "Background_Color_Hover": "#4aa5ff", + "Border_Color": "#787878", + "FontColor": ReturnFontColor(), + }, + } +} 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 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"