diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 229b4955..1d7b1b77 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,7 +5,7 @@ on: - 'v*' jobs: - build-macos: + build-macos-Qt5: runs-on: macos-11 steps: - @@ -32,7 +32,7 @@ jobs: cd $GITHUB_WORKSPACE cp ${{ steps.setup_ffmpeg.outputs.ffmpeg-path }} bin/ cp static/build_files/macos/pyoxidizer.bzl pyoxidizer.bzl - cp static/build_files/macos/requirements.txt requirements.txt + cp static/build_files/macos/requirementsQt5.txt requirements.txt basename $(rustc --print sysroot) | sed -e "s/^stable-//" > triple.txt pyoxidizer build --release cd build/$(head -n 1 triple.txt)/release @@ -49,17 +49,71 @@ jobs: cd $GITHUB_WORKSPACE temp_dmg="$(mktemp).dmg" hdiutil create "$temp_dmg" -ov -volname "HydrusNetwork" -fs HFS+ -srcfolder "$GITHUB_WORKSPACE/build/$(head -n 1 triple.txt)/release" - hdiutil convert "$temp_dmg" -format UDZO -o HydrusNetwork.dmg + hdiutil convert "$temp_dmg" -format UDZO -o HydrusNetwork5.dmg - name: Upload a Build Artifact uses: actions/upload-artifact@v2.2.1 with: name: MacOS-DMG - path: HydrusNetwork.dmg + path: HydrusNetwork5.dmg if-no-files-found: error retention-days: 2 - build-ubuntu: + build-macos-Qt6: + runs-on: macos-11 + steps: + - + name: Checkout + uses: actions/checkout@v2 + - + name: Setup FFMPEG + uses: FedericoCarboni/setup-ffmpeg@v1 + id: setup_ffmpeg + with: + token: ${{ secrets.GITHUB_TOKEN }} + - + name: Install mkdocs-material + run: python3 -m pip install mkdocs-material + - + name: Build docs to /help + run: mkdocs build -d help + - + name: Install PyOxidizer + run: python3 -m pip install pyoxidizer + - + name: Build Hydrus + run: | + cd $GITHUB_WORKSPACE + cp ${{ steps.setup_ffmpeg.outputs.ffmpeg-path }} bin/ + cp static/build_files/macos/pyoxidizer.bzl pyoxidizer.bzl + cp static/build_files/macos/requirementsQt6.txt requirements.txt + basename $(rustc --print sysroot) | sed -e "s/^stable-//" > triple.txt + pyoxidizer build --release + cd build/$(head -n 1 triple.txt)/release + mkdir -p "Hydrus Network.app/Contents/MacOS" + mkdir -p "Hydrus Network.app/Contents/Resources" + mkdir -p "Hydrus Network.app/Contents/Frameworks" + mv install/static/icon.icns "Hydrus Network.app/Contents/Resources/icon.icns" + cp install/static/build_files/macos/Info.plist "Hydrus Network.app/Contents/Info.plist" + cp install/static/build_files/macos/ReadMeFirst.rtf ./ReadMeFirst.rtf + cp install/static/build_files/macos/running_from_app "install/running_from_app" + ln -s /Applications ./Applications + mv install/* "Hydrus Network.app/Contents/MacOS/" + rm -rf install + cd $GITHUB_WORKSPACE + temp_dmg="$(mktemp).dmg" + hdiutil create "$temp_dmg" -ov -volname "HydrusNetwork" -fs HFS+ -srcfolder "$GITHUB_WORKSPACE/build/$(head -n 1 triple.txt)/release" + hdiutil convert "$temp_dmg" -format UDZO -o HydrusNetwork6.dmg + - + name: Upload a Build Artifact + uses: actions/upload-artifact@v2.2.1 + with: + name: MacOS-DMG + path: HydrusNetwork6.dmg + if-no-files-found: error + retention-days: 2 + + build-ubuntu-Qt5: runs-on: ubuntu-18.04 steps: - @@ -104,7 +158,7 @@ jobs: uses: BSFishy/pip-action@v1 with: packages: pyinstaller - requirements: hydrus/static/build_files/linux/requirements.txt + requirements: hydrus/static/build_files/linux/requirementsQt5.txt - name: Build Hydrus run: | @@ -127,17 +181,95 @@ jobs: name: Compress Client run: | mv dist/client "dist/Hydrus Network" - tar -czvf Ubuntu-Extract.tar.gz -C dist "Hydrus Network" + tar -czvf Ubuntu-Extract5.tar.gz -C dist "Hydrus Network" - name: Upload a Build Artifact uses: actions/upload-artifact@v2 with: name: Ubuntu-Extract - path: Ubuntu-Extract.tar.gz + path: Ubuntu-Extract5.tar.gz if-no-files-found: error retention-days: 2 - build-windows: + build-ubuntu-Qt6: + runs-on: ubuntu-20.04 + steps: + - + name: Checkout + uses: actions/checkout@v2 + with: + path: hydrus + - + name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + architecture: x64 + - + name: Install mkdocs-material + run: pip install mkdocs-material + - + name: Build docs to /help + run: mkdocs build -d help + working-directory: hydrus + #- name: Cache Qt + # id: cache-qt + # uses: actions/cache@v1 + # with: + # path: Qt + # key: ${{ runner.os }}-QtCache + #- + # name: Install Qt + # uses: jurplel/install-qt-action@v2 + # with: + # install-deps: true + # setup-python: 'false' + # modules: qtcharts qtwidgets qtgui qtcore + # cached: ${{ steps.cache-qt.outputs.cache-hit }} + - + name: APT Install + run: | + sudo apt-get update + sudo apt-get install -y libmpv1 + - + name: Pip Installer + uses: BSFishy/pip-action@v1 + with: + packages: pyinstaller + requirements: hydrus/static/build_files/linux/requirementsQt6.txt + - + name: Build Hydrus + run: | + cp hydrus/static/build_files/linux/client.spec client.spec + cp hydrus/static/build_files/linux/server.spec server.spec + pyinstaller server.spec + pyinstaller client.spec + - + name: Remove Chonk + run: | + find dist/client/ -type f -name "*.pyc" -delete + while read line; do find dist/client/ -type f -name "${line}" -delete ; done < hydrus/static/build_files/linux/files_to_delete.txt + - + name: Set Permissions + run: | + sudo chown --recursive 1000:1000 dist/client + sudo find dist/client -type d -exec chmod 0755 {} \; + sudo chmod +x dist/client/client dist/client/server dist/client/bin/swfrender_linux + - + name: Compress Client + run: | + mv dist/client "dist/Hydrus Network" + tar -czvf Ubuntu-Extract6.tar.gz -C dist "Hydrus Network" + - + name: Upload a Build Artifact + uses: actions/upload-artifact@v2 + with: + name: Ubuntu-Extract + path: Ubuntu-Extract6.tar.gz + if-no-files-found: error + retention-days: 2 + + build-windows-Qt5: runs-on: windows-2019 steps: - @@ -184,7 +316,7 @@ jobs: uses: BSFishy/pip-action@v1 with: packages: pyinstaller - requirements: hydrus\static\build_files\windows\requirements.txt + requirements: hydrus\static\build_files\windows\requirementsQt5.txt - name: Download mpv-dev uses: carlosperate/download-file-action@v1.0.3 @@ -220,7 +352,7 @@ jobs: name: Compress Client run: | cd .\dist - 7z.exe a -tzip -mm=Deflate -mx=5 ..\Windows-Extract.zip 'Hydrus Network' + 7z.exe a -tzip -mm=Deflate -mx=5 ..\Windows-Extract5.zip 'Hydrus Network' cd .. - name: Upload a Build Artifact @@ -235,14 +367,101 @@ jobs: uses: actions/upload-artifact@v2 with: name: Windows-Extract - path: Windows-Extract.zip + path: Windows-Extract5.zip + if-no-files-found: error + retention-days: 2 + + build-windows-Qt6: + runs-on: windows-2019 + steps: + - + name: Checkout + uses: actions/checkout@v2 + with: + path: hydrus + - + name: Setup FFMPEG + uses: FedericoCarboni/setup-ffmpeg@v1 + id: setup_ffmpeg + with: + token: ${{ secrets.GITHUB_TOKEN }} + - + name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + architecture: x64 + - + name: Install mkdocs-material + run: pip install mkdocs-material + - + name: Build docs to /help + run: mkdocs build -d help + working-directory: hydrus + - + name: Cache Qt + id: cache_qt + uses: actions/cache@v1 + with: + path: ../Qt + key: ${{ runner.os }}-QtCache + - + name: Install Qt + uses: jurplel/install-qt-action@v2 + with: + install-deps: true + setup-python: 'false' + modules: qtcharts qtwidgets qtgui qtcore + cached: ${{ steps.cache_qt.outputs.cache-hit }} + - + name: PIP Install Packages + uses: BSFishy/pip-action@v1 + with: + packages: pyinstaller + requirements: hydrus\static\build_files\windows\requirementsQt6.txt + - + name: Download mpv-dev + uses: carlosperate/download-file-action@v1.0.3 + id: download_mpv + with: + file-url: 'https://sourceforge.net/projects/mpv-player-windows/files/libmpv/mpv-dev-x86_64-20210228-git-d1be8bb.7z' + file-name: 'mpv-dev-x86_64.7z' + location: '.' + - + name: Process mpv-dev + run: | + 7z x ${{ steps.download_mpv.outputs.file-path }} + move mpv-1.dll hydrus\ + - + name: Build Hydrus + run: | + move ${{ steps.setup_ffmpeg.outputs.ffmpeg-path }} hydrus\bin\ + move hydrus\static\build_files\windows\sqlite3.dll hydrus\ + move hydrus\static\build_files\windows\sqlite3.exe hydrus\db + move hydrus\static\build_files\windows\client-win.spec client-win.spec + move hydrus\static\build_files\windows\server-win.spec server-win.spec + pyinstaller server-win.spec + pyinstaller client-win.spec + dir -r + - + name: Compress Client + run: | + cd .\dist + 7z.exe a -tzip -mm=Deflate -mx=5 ..\Windows-Extract6.zip 'Hydrus Network' + cd .. + - + name: Upload a Build Artifact + uses: actions/upload-artifact@v2 + with: + name: Windows-Extract + path: Windows-Extract6.zip if-no-files-found: error retention-days: 2 create-release: name: Create Release Entry runs-on: ubuntu-20.04 - needs: [build-windows, build-ubuntu, build-macos] + needs: [build-windows-Qt5, build-windows-Qt6, build-ubuntu-Qt5, build-ubuntu-Qt6, build-macos-Qt5, build-macos-Qt6] steps: - name: Checkout code @@ -260,19 +479,25 @@ jobs: name: Rename Files run: | mkdir ubuntu windows - mv MacOS-DMG/HydrusNetwork.dmg Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.macOS.-.App.dmg - mv Windows-Install/HydrusInstaller.exe Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Windows.-.Installer.exe - mv Windows-Extract/Windows-Extract.zip Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Windows.-.Extract.only.zip - mv Ubuntu-Extract/Ubuntu-Extract.tar.gz Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Linux.-.Executable.tar.gz + mv MacOS-DMG/HydrusNetwork5.dmg Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.macOS.Qt5.-.App.dmg + mv MacOS-DMG/HydrusNetwork6.dmg Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.macOS.Qt6.-.App.dmg + mv Windows-Install/HydrusInstaller.exe Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Windows.Qt5.-.Installer.exe + mv Windows-Extract/Windows-Extract5.zip Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Windows.Qt5.-.Extract.only.zip + mv Windows-Extract/Windows-Extract6.zip Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Windows.Qt6.-.Extract.only.zip + mv Ubuntu-Extract/Ubuntu-Extract5.tar.gz Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Linux.Qt5.-.Executable.tar.gz + mv Ubuntu-Extract/Ubuntu-Extract6.tar.gz Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Linux.Qt6.-.Executable.tar.gz - name: Release new uses: softprops/action-gh-release@v1 if: startsWith(github.ref, 'refs/tags/') with: files: | - Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Windows.-.Installer.exe - Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Windows.-.Extract.only.zip - Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Linux.-.Executable.tar.gz - Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.macOS.-.App.dmg + Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Windows.Qt5.-.Installer.exe + Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Windows.Qt5.-.Extract.only.zip + Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Windows.Qt6.-.Extract.only.zip + Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Linux.Qt5.-.Executable.tar.gz + Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Linux.Qt6.-.Executable.tar.gz + Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.macOS.Qt5.-.App.dmg + Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.macOS.Qt6.-.App.dmg env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/docs/running_from_source.md b/docs/running_from_source.md index 5a240ff6..475587f4 100644 --- a/docs/running_from_source.md +++ b/docs/running_from_source.md @@ -64,14 +64,20 @@ pip3 install -r requirements_windows.txt If you prefer to do things manually, inspect the document and install the modules yourself. -## PyQt5 support { id="pyqt5" } +## Qt { id="qt" } -For Qt, either PySide2 (default) or PyQt5 are supported, through qtpy. For PyQt5, go: +Qt is the UI library. You can run PySide2, PySide6, PyQt5, or PyQt6. A wrapper library called `qtpy` allows this. The default for now is PySide2, but it will soon be PySide6. For PyQt5 or PyQt6, go: ``` pip3 install qtpy PyQtChart PyQt5 +-or- +pip3 install qtpy PyQt6-Charts PyQt6 ``` +If you have multiple Qts installed, then select which one you want to use by setting the `QT_API` environment variable to 'pyside2', 'pyside6', 'pyqt5', or 'pyqt6'. Check _help->about_ to make sure it loaded the right one. + +If you run Windows 7, you cannot run Qt6. Please try PySide2 or PyQt5. + ## FFMPEG { id="ffmpeg" } If you don't have FFMPEG in your PATH and you want to import anything more fun than jpegs, you will need to put a static [FFMPEG](https://ffmpeg.org/) executable in your PATH or the `install_dir/bin` directory. If you can't find a static exe on Windows, you can copy the exe from one of my extractable releases. diff --git a/hydrus/client/ClientController.py b/hydrus/client/ClientController.py index fa0c4b66..28391177 100644 --- a/hydrus/client/ClientController.py +++ b/hydrus/client/ClientController.py @@ -42,6 +42,7 @@ from hydrus.client.gui import ClientGUIScrolledPanelsManagement from hydrus.client.gui import ClientGUISplash from hydrus.client.gui import ClientGUIStyle from hydrus.client.gui import ClientGUITopLevelWindowsPanels +from hydrus.client.gui import QtInit from hydrus.client.gui import QtPorting as QP from hydrus.client.gui.lists import ClientGUIListManager from hydrus.client.importing import ClientImportSubscriptions @@ -1588,7 +1589,7 @@ class Controller( HydrusController.HydrusController ): def Run( self ): - QP.MonkeyPatchMissingMethods() + QtInit.MonkeyPatchMissingMethods() from hydrus.client.gui import ClientGUICore diff --git a/hydrus/client/gui/ClientGUI.py b/hydrus/client/gui/ClientGUI.py index e8dabe52..e085585f 100644 --- a/hydrus/client/gui/ClientGUI.py +++ b/hydrus/client/gui/ClientGUI.py @@ -76,6 +76,7 @@ from hydrus.client.gui import ClientGUITopLevelWindows from hydrus.client.gui import ClientGUITopLevelWindowsPanels from hydrus.client.gui import QLocator from hydrus.client.gui import ClientGUILocatorSearchProviders +from hydrus.client.gui import QtInit from hydrus.client.gui import QtPorting as QP from hydrus.client.gui.canvas import ClientGUIMPV from hydrus.client.gui.networking import ClientGUIHydrusNetwork @@ -696,38 +697,45 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes, CAC.ApplicationCo library_versions.append( ( 'Qt', QC.__version__ ) ) - if qtpy.PYSIDE2: + if QtInit.WE_ARE_QT5: - import PySide2 - import shiboken2 + if QtInit.WE_ARE_PYSIDE: + + import PySide2 + import shiboken2 + + library_versions.append( ( 'PySide2', PySide2.__version__ ) ) + library_versions.append( ( 'shiboken2', shiboken2.__version__ ) ) + + elif QtInit.WE_ARE_PYQT: + + from PyQt5.Qt import PYQT_VERSION_STR # pylint: disable=E0401,E0611 + from PyQt5.sip import SIP_VERSION_STR # pylint: disable=E0401 + + library_versions.append( ( 'PyQt5', PYQT_VERSION_STR ) ) + library_versions.append( ( 'sip', SIP_VERSION_STR ) ) + - library_versions.append( ( 'PySide2', PySide2.__version__ ) ) - library_versions.append( ( 'shiboken2', shiboken2.__version__ ) ) + elif QtInit.WE_ARE_QT6: - elif qtpy.PYQT5: - - from PyQt5.Qt import PYQT_VERSION_STR # pylint: disable=E0401,E0611 - from PyQt5.sip import SIP_VERSION_STR # pylint: disable=E0401 - - library_versions.append( ( 'PyQt5', PYQT_VERSION_STR ) ) - library_versions.append( ( 'sip', SIP_VERSION_STR ) ) + if QtInit.WE_ARE_PYSIDE: + + import PySide6 + import shiboken6 + + library_versions.append( ( 'PySide6', PySide6.__version__ ) ) + library_versions.append( ( 'shiboken6', shiboken6.__version__ ) ) + + elif QtInit.WE_ARE_PYQT: + + from PyQt6.QtCore import PYQT_VERSION_STR # pylint: disable=E0401 + from PyQt6.sip import SIP_VERSION_STR # pylint: disable=E0401 + + library_versions.append( ( 'PyQt6', PYQT_VERSION_STR ) ) + library_versions.append( ( 'sip', SIP_VERSION_STR ) ) + - elif QP.WE_ARE_PYSIDE and QP.WE_ARE_QT6: - - import PySide6 - import shiboken6 - - library_versions.append( ( 'PySide6', PySide6.__version__ ) ) - library_versions.append( ( 'shiboken6', shiboken6.__version__ ) ) - - elif QP.WE_ARE_PYQT and QP.WE_ARE_QT6: - - from PyQt6.QtCore import PYQT_VERSION_STR # pylint: disable=E0401 - from PyQt6.sip import SIP_VERSION_STR # pylint: disable=E0401 - - library_versions.append( ( 'PyQt6', PYQT_VERSION_STR ) ) - library_versions.append( ( 'sip', SIP_VERSION_STR ) ) - + CBOR_AVAILABLE = False try: diff --git a/hydrus/client/gui/ClientGUIFunctions.py b/hydrus/client/gui/ClientGUIFunctions.py index f099b852..2d1b2c29 100644 --- a/hydrus/client/gui/ClientGUIFunctions.py +++ b/hydrus/client/gui/ClientGUIFunctions.py @@ -9,6 +9,7 @@ from hydrus.core import HydrusConstants as HC from hydrus.core import HydrusGlobals as HG from hydrus.core import HydrusText +from hydrus.client.gui import QtInit from hydrus.client.gui import QtPorting as QP def ClientToScreen( win: QW.QWidget, pos: QC.QPoint ) -> QC.QPoint: @@ -84,11 +85,11 @@ def ConvertQtImageToNumPy( qt_image: QG.QImage ): data_bytearray = qt_image.bits() - if QP.WE_ARE_PYSIDE: + if QtInit.WE_ARE_PYSIDE: data_bytes = bytes( data_bytearray ) - elif QP.WE_ARE_PYQT: + elif QtInit.WE_ARE_PYQT: data_bytes = data_bytearray.asstring( height * width * depth ) diff --git a/hydrus/client/gui/ClientGUIRatings.py b/hydrus/client/gui/ClientGUIRatings.py index d527fc71..20ecdec7 100644 --- a/hydrus/client/gui/ClientGUIRatings.py +++ b/hydrus/client/gui/ClientGUIRatings.py @@ -348,9 +348,9 @@ class RatingNumerical( QW.QWidget ): def _GetRatingStateAndRatingFromClickEvent( self, event ): - click_pos = event.pos() + click_pos = event.position().toPoint() - x = event.pos().x() + x = click_pos.x() BORDER = 1 diff --git a/hydrus/client/gui/ClientGUIShortcuts.py b/hydrus/client/gui/ClientGUIShortcuts.py index 80e7e48e..6fa6f375 100644 --- a/hydrus/client/gui/ClientGUIShortcuts.py +++ b/hydrus/client/gui/ClientGUIShortcuts.py @@ -1273,7 +1273,7 @@ class ShortcutsHandler( QC.QObject ): if event.type() == QC.QEvent.MouseButtonPress: - self._last_click_down_position = event.globalPos() + self._last_click_down_position = event.globalPosition().toPoint() CUMULATIVE_MOUSEWARP_MANHATTAN_LENGTH = 0 @@ -1290,7 +1290,7 @@ class ShortcutsHandler( QC.QObject ): if event.type() == QC.QEvent.MouseButtonRelease: - release_press_pos = event.globalPos() + release_press_pos = event.globalPosition().toPoint() delta = release_press_pos - self._last_click_down_position diff --git a/hydrus/client/gui/QtInit.py b/hydrus/client/gui/QtInit.py new file mode 100644 index 00000000..4ad45dbc --- /dev/null +++ b/hydrus/client/gui/QtInit.py @@ -0,0 +1,135 @@ +import os + +# If not explicitly set, prefer PySide instead of PyQt5, which is the qtpy default +# It is critical that this runs on startup *before* anything is imported from qtpy. + +if 'QT_API' not in os.environ: + + try: + + import PySide2 # Qt5 + + os.environ[ 'QT_API' ] = 'pyside2' + + except ImportError as e: + + try: + + import PySide6 # Qt6 + + os.environ[ 'QT_API' ] = 'pyside6' + + except ImportError as e: + + pass + + + + +# + +import qtpy + +from qtpy import QtCore as QC +from qtpy import QtWidgets as QW +from qtpy import QtGui as QG + +# 2022-07 +# an older version of qtpy, 1.9 or so, didn't actually have attribute qtpy.PYQT6, so we'll test and assign carefully + +WE_ARE_QT5 = False +WE_ARE_QT6 = False + +WE_ARE_PYQT = False +WE_ARE_PYSIDE = False + +if qtpy.PYQT5: + + WE_ARE_QT5 = True + WE_ARE_PYQT = True + + from PyQt5 import sip # pylint: disable=E0401 + + def isValid( obj ): + + if isinstance( obj, sip.simplewrapper ): + + return not sip.isdeleted( obj ) + + + return True + + +elif hasattr( qtpy, 'PYQT6' ) and qtpy.PYQT6: + + WE_ARE_QT6 = True + WE_ARE_PYQT = True + + from PyQt6 import sip # pylint: disable=E0401 + + def isValid( obj ): + + if isinstance( obj, sip.simplewrapper ): + + return not sip.isdeleted( obj ) + + + return True + +elif qtpy.PYSIDE2: + + WE_ARE_QT5 = True + WE_ARE_PYSIDE = True + + import shiboken2 + + isValid = shiboken2.isValid + +elif qtpy.PYSIDE6: + + WE_ARE_QT6 = True + WE_ARE_PYSIDE = True + + import shiboken6 + + isValid = shiboken6.isValid + +else: + + raise RuntimeError( 'You need one of PySide2, PySide6, PyQt5, or PyQt6' ) + + +def MonkeyPatchMissingMethods(): + + if WE_ARE_QT5: + + QG.QMouseEvent.globalPosition = lambda self, *args, **kwargs: QC.QPointF( self.globalPos( *args, **kwargs ) ) + + QG.QMouseEvent.position = lambda self, *args, **kwargs: QC.QPointF( self.pos( *args, **kwargs ) ) + + QG.QDropEvent.position = lambda self, *args, **kwargs: QC.QPointF( self.pos( *args, **kwargs ) ) + + QG.QDropEvent.modifiers = lambda self, *args, **kwargs: self.keyboardModifiers( *args, **kwargs ) + + + if WE_ARE_PYQT: + + def MonkeyPatchGetSaveFileName( original_function ): + + def new_function( *args, **kwargs ): + + if 'selectedFilter' in kwargs: + + kwargs[ 'initialFilter' ] = kwargs[ 'selectedFilter' ] + del kwargs[ 'selectedFilter' ] + + return original_function( *args, **kwargs ) + + + + return new_function + + + QW.QFileDialog.getSaveFileName = MonkeyPatchGetSaveFileName( QW.QFileDialog.getSaveFileName ) + + diff --git a/hydrus/client/gui/QtPorting.py b/hydrus/client/gui/QtPorting.py index 9f780ee7..06214c10 100644 --- a/hydrus/client/gui/QtPorting.py +++ b/hydrus/client/gui/QtPorting.py @@ -2,32 +2,6 @@ import os -# If not explicitly set, prefer PySide2/PySide6 instead of the qtpy default which is PyQt5 -# It is important that this runs on startup *before* anything is imported from qtpy. -# Since test.py, client.py and client.pyw all import this module first before any other Qt related ones, this requirement is satisfied. - -if not 'QT_API' in os.environ: - - try: - - import PySide2 # Qt5 - - os.environ[ 'QT_API' ] = 'pyside2' - - except ImportError as e: - - try: - - import PySide6 # Qt6 - - os.environ[ 'QT_API' ] = 'pyside6' - - except ImportError as e: - - pass - - -# import qtpy from qtpy import QtCore as QC from qtpy import QtWidgets as QW @@ -37,113 +11,26 @@ import math from collections import defaultdict -# we can't test qtpy.PYQT6 unless it has it lmao, so we'll test and assign more carefully - -WE_ARE_QT5 = False -WE_ARE_QT6 = False - -WE_ARE_PYQT = False -WE_ARE_PYSIDE = False - -if qtpy.PYQT5: - - from PyQt5 import sip # pylint: disable=E0401 - - WE_ARE_QT5 = True - WE_ARE_PYQT = True - - def isValid( obj ): - - if isinstance( obj, sip.simplewrapper ): - - return not sip.isdeleted( obj ) - - - return True - - -elif hasattr( qtpy, 'PYQT6' ) and qtpy.PYQT6: - - from PyQt6 import sip # pylint: disable=E0401 - - WE_ARE_QT6 = True - WE_ARE_PYQT = True - - def isValid( obj ): - - if isinstance( obj, sip.simplewrapper ): - - return not sip.isdeleted( obj ) - - - return True - -elif qtpy.PYSIDE2: - - import shiboken2 - - WE_ARE_QT5 = True - WE_ARE_PYSIDE = True - - isValid = shiboken2.isValid - -elif qtpy.PYSIDE6: - - import shiboken6 - - WE_ARE_QT6 = True - WE_ARE_PYSIDE = True - - isValid = shiboken6.isValid - -else: - - raise RuntimeError( 'You need one of PySide2, PySide6, PyQt5 or PyQt6' ) - - from hydrus.core import HydrusConstants as HC from hydrus.core import HydrusData from hydrus.core import HydrusGlobals as HG from hydrus.client import ClientConstants as CC +from hydrus.client.gui import QtInit -def MonkeyPatchMissingMethods(): - - if WE_ARE_QT5: - - QG.QMouseEvent.globalPosition = lambda self, *args, **kwargs: self.globalPos( *args, **kwargs ) - QG.QDropEvent.position = lambda self, *args, **kwargs: self.posF( *args, **kwargs ) - - - if WE_ARE_PYQT: - - def MonkeyPatchGetSaveFileName( original_function ): - - def new_function( *args, **kwargs ): - - if 'selectedFilter' in kwargs: - - kwargs[ 'initialFilter' ] = kwargs[ 'selectedFilter' ] - del kwargs[ 'selectedFilter' ] - - return original_function( *args, **kwargs ) - - - - return new_function - - - QW.QFileDialog.getSaveFileName = MonkeyPatchGetSaveFileName( QW.QFileDialog.getSaveFileName ) - +isValid = QtInit.isValid def registerEventType(): - if qtpy.PYSIDE2 or qtpy.PYSIDE6: + if QtInit.WE_ARE_PYSIDE: return QC.QEvent.Type( QC.QEvent.registerEventType() ) - return QC.QEvent.registerEventType() - + else: + + return QC.QEvent.registerEventType() + + class HBoxLayout( QW.QHBoxLayout ): @@ -159,6 +46,7 @@ class HBoxLayout( QW.QHBoxLayout ): self.setContentsMargins( val, val, val, val ) + class VBoxLayout( QW.QVBoxLayout ): @@ -465,13 +353,13 @@ class TabBar( QW.QTabBar ): def mousePressEvent( self, event ): - index = self.tabAt( event.pos() ) + index = self.tabAt( event.position().toPoint() ) if event.button() == QC.Qt.LeftButton: self._last_clicked_tab_index = index - self._last_clicked_global_pos = event.globalPosition() + self._last_clicked_global_pos = event.globalPosition().toPoint() QW.QTabBar.mousePressEvent( self, event ) @@ -479,7 +367,7 @@ class TabBar( QW.QTabBar ): def mouseReleaseEvent( self, event ): - index = self.tabAt( event.pos() ) + index = self.tabAt( event.position().toPoint() ) if event.button() == QC.Qt.MiddleButton: @@ -496,7 +384,7 @@ class TabBar( QW.QTabBar ): def mouseDoubleClickEvent( self, event ): - index = self.tabAt( event.pos() ) + index = self.tabAt( event.position().toPoint() ) if event.button() == QC.Qt.LeftButton: @@ -649,7 +537,7 @@ class TabWidgetWithDnD( QW.QTabWidget ): def mouseMoveEvent( self, e ): - if self.currentWidget() and self.currentWidget().rect().contains( self.currentWidget().mapFromGlobal( self.mapToGlobal( e.pos() ) ) ): + if self.currentWidget() and self.currentWidget().rect().contains( self.currentWidget().mapFromGlobal( self.mapToGlobal( e.position().toPoint() ) ) ): QW.QTabWidget.mouseMoveEvent( self, e ) @@ -659,7 +547,7 @@ class TabWidgetWithDnD( QW.QTabWidget ): return - my_mouse_pos = e.pos() + my_mouse_pos = e.position().toPoint() global_mouse_pos = self.mapToGlobal( my_mouse_pos ) tab_bar_mouse_pos = self._tab_bar.mapFromGlobal( global_mouse_pos ) @@ -680,7 +568,7 @@ class TabWidgetWithDnD( QW.QTabWidget ): return - if e.globalPosition() == clicked_global_pos: + if e.globalPosition().toPoint() == clicked_global_pos: # don't start a drag until movement @@ -714,7 +602,7 @@ class TabWidgetWithDnD( QW.QTabWidget ): drag.exec_( QC.Qt.MoveAction ) - def dragEnterEvent( self, e ): + def dragEnterEvent( self, e: QG.QDragEnterEvent ): if self.currentWidget() and self.currentWidget().rect().contains( self.currentWidget().mapFromGlobal( self.mapToGlobal( e.position().toPoint() ) ) ): @@ -731,11 +619,11 @@ class TabWidgetWithDnD( QW.QTabWidget ): - def dragMoveEvent( self, event ): + def dragMoveEvent( self, event: QG.QDragMoveEvent ): - #if self.currentWidget() and self.currentWidget().rect().contains( self.currentWidget().mapFromGlobal( self.mapToGlobal( event.pos() ) ) ): return QW.QTabWidget.dragMoveEvent( self, event ) + #if self.currentWidget() and self.currentWidget().rect().contains( self.currentWidget().mapFromGlobal( self.mapToGlobal( event.position().toPoint() ) ) ): return QW.QTabWidget.dragMoveEvent( self, event ) - screen_pos = self.mapToGlobal( event.pos() ) + screen_pos = self.mapToGlobal( event.position().toPoint() ) tab_pos = self._tab_bar.mapFromGlobal( screen_pos ) @@ -743,7 +631,7 @@ class TabWidgetWithDnD( QW.QTabWidget ): if tab_index != -1: - shift_down = event.keyboardModifiers() & QC.Qt.ShiftModifier + shift_down = event.modifiers() & QC.Qt.ShiftModifier self.setCurrentIndex( tab_index ) @@ -756,9 +644,9 @@ class TabWidgetWithDnD( QW.QTabWidget ): #return QW.QTabWidget.dragMoveEvent( self, event ) - def dragLeaveEvent( self, e ): + def dragLeaveEvent( self, e: QG.QDragLeaveEvent ): - #if self.currentWidget() and self.currentWidget().rect().contains( self.currentWidget().mapFromGlobal( self.mapToGlobal( e.pos() ) ) ): return QW.QTabWidget.dragLeaveEvent( self, e ) + #if self.currentWidget() and self.currentWidget().rect().contains( self.currentWidget().mapFromGlobal( self.mapToGlobal( e.position().toPoint() ) ) ): return QW.QTabWidget.dragLeaveEvent( self, e ) e.accept() @@ -783,13 +671,13 @@ class TabWidgetWithDnD( QW.QTabWidget ): QW.QTabWidget.insertTab( self, index, widget, *args, **kwargs ) - def dropEvent( self, e ): + def dropEvent( self, e: QG.QDropEvent ): - if self.currentWidget() and self.currentWidget().rect().contains( self.currentWidget().mapFromGlobal( self.mapToGlobal( e.pos() ) ) ): + if self.currentWidget() and self.currentWidget().rect().contains( self.currentWidget().mapFromGlobal( self.mapToGlobal( e.position().toPoint() ) ) ): return QW.QTabWidget.dropEvent( self, e ) - + if 'application/hydrus-tab' not in e.mimeData().formats(): #Page dnd has no associated mime data e.ignore() @@ -832,7 +720,7 @@ class TabWidgetWithDnD( QW.QTabWidget ): counter = self.count() - screen_pos = self.mapToGlobal( e.pos() ) + screen_pos = self.mapToGlobal( e.position().toPoint() ) tab_pos = self.tabBar().mapFromGlobal( screen_pos ) @@ -857,8 +745,10 @@ class TabWidgetWithDnD( QW.QTabWidget ): left_edge_rect = QC.QRect( tab_rect.topLeft(), edge_size ) right_edge_rect = QC.QRect( tab_rect.topRight() - QC.QPoint( EDGE_PADDING, 0 ), edge_size ) - dropped_on_left_edge = left_edge_rect.contains( e.pos() ) - dropped_on_right_edge = right_edge_rect.contains( e.pos() ) + drop_pos = e.position().toPoint() + + dropped_on_left_edge = left_edge_rect.contains( drop_pos ) + dropped_on_right_edge = right_edge_rect.contains( drop_pos ) if counter == 0: @@ -902,8 +792,8 @@ class TabWidgetWithDnD( QW.QTabWidget ): self.insertTab( insert_index, source_page, source_name ) - shift_down = e.keyboardModifiers() & QC.Qt.ShiftModifier - + shift_down = e.modifiers() & QC.Qt.ShiftModifier + follow_dropped_page = not shift_down new_options = HG.client_controller.new_options diff --git a/hydrus/client/gui/lists/ClientGUIListBoxes.py b/hydrus/client/gui/lists/ClientGUIListBoxes.py index 47863fcd..0e50306d 100644 --- a/hydrus/client/gui/lists/ClientGUIListBoxes.py +++ b/hydrus/client/gui/lists/ClientGUIListBoxes.py @@ -1119,7 +1119,7 @@ class ListBox( QW.QScrollArea ): def _GetLogicalIndexUnderMouse( self, mouse_event ): - y = mouse_event.pos().y() + y = mouse_event.position().toPoint().y() if mouse_event.type() == QC.QEvent.MouseMove: diff --git a/hydrus/client/gui/pages/ClientGUIResults.py b/hydrus/client/gui/pages/ClientGUIResults.py index e9d75393..eb288d83 100644 --- a/hydrus/client/gui/pages/ClientGUIResults.py +++ b/hydrus/client/gui/pages/ClientGUIResults.py @@ -2916,8 +2916,10 @@ class MediaPanelThumbnails( MediaPanel ): def _GetThumbnailUnderMouse( self, mouse_event ): - x = mouse_event.pos().x() - y = mouse_event.pos().y() + pos = mouse_event.position().toPoint() + + x = pos.x() + y = pos.y() ( t_span_x, t_span_y ) = self._GetThumbnailSpanDimensions() diff --git a/hydrus/hydrus_client.py b/hydrus/hydrus_client.py index 18277e86..1651aa89 100644 --- a/hydrus/hydrus_client.py +++ b/hydrus/hydrus_client.py @@ -20,7 +20,7 @@ try: HydrusBoot.AddBaseDirToEnvPath() # initialise Qt here, important it is done early - from hydrus.client.gui import QtPorting as QP + from hydrus.client.gui import QtInit from hydrus.core import HydrusConstants as HC from hydrus.core import HydrusData diff --git a/hydrus/hydrus_test.py b/hydrus/hydrus_test.py index 0a07c6b9..ae12f244 100644 --- a/hydrus/hydrus_test.py +++ b/hydrus/hydrus_test.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 +from hydrus.client.gui import QtInit from hydrus.client.gui import QtPorting as QP from qtpy import QtWidgets as QW @@ -36,7 +37,7 @@ def boot(): threading.Thread( target = reactor.run, kwargs = { 'installSignalHandlers' : 0 } ).start() - QP.MonkeyPatchMissingMethods() + QtInit.MonkeyPatchMissingMethods() app = QW.QApplication( sys.argv ) app.call_after_catcher = QP.CallAfterEventCatcher( app ) diff --git a/static/build_files/docker/docker_build.yml b/static/build_files/docker/docker_build.yml index cfb93e65..d39b74fa 100644 --- a/static/build_files/docker/docker_build.yml +++ b/static/build_files/docker/docker_build.yml @@ -3,11 +3,10 @@ on: push: tags: - 'v*' - workflow_dispatch: [] jobs: build-client: - runs-on: [ubuntu-latest] + runs-on: ubuntu-latest steps: - name: Checkout @@ -53,7 +52,7 @@ jobs: labels: ${{ steps.docker_meta.outputs.labels }} build-server: - runs-on: [ubuntu-latest] + runs-on: ubuntu-latest steps: - name: Checkout @@ -99,4 +98,4 @@ jobs: file: ./static/build_files/docker/server/Dockerfile platforms: linux/amd64,linux/386,linux/arm/v6,linux/arm/v7,linux/arm64 tags: ${{ steps.docker_meta.outputs.tags }} - labels: ${{ steps.docker_meta.outputs.labels }} \ No newline at end of file + labels: ${{ steps.docker_meta.outputs.labels }} diff --git a/static/build_files/linux/requirements.txt b/static/build_files/linux/requirementsQt5.txt similarity index 100% rename from static/build_files/linux/requirements.txt rename to static/build_files/linux/requirementsQt5.txt diff --git a/static/build_files/linux/requirementsQt6.txt b/static/build_files/linux/requirementsQt6.txt new file mode 100644 index 00000000..40b65098 --- /dev/null +++ b/static/build_files/linux/requirementsQt6.txt @@ -0,0 +1,24 @@ +beautifulsoup4>=4.0.0 +cbor2 +chardet>=3.0.4 +cloudscraper>=1.2.33 +html5lib>=1.0.1 +lxml>=4.5.0 +lz4>=3.0.0 +nose>=1.3.0 +numpy>=1.16.0 +opencv-python-headless>=4.0.0, <=4.5.3.56 +Pillow>=6.0.0 +psutil>=5.0.0 +pylzma>=0.5.0 +pyOpenSSL>=19.1.0 +PySide6>=6.0.0 +PySocks>=1.7.0 +python-mpv==0.4.5 +PyYAML>=5.0.0 +QtPy>=1.9.0 +requests==2.23.0 +Send2Trash>=1.5.0 +service-identity>=18.1.0 +six>=1.14.0 +Twisted>=20.3.0 diff --git a/static/build_files/macos/requirements.txt b/static/build_files/macos/requirementsQt5.txt similarity index 100% rename from static/build_files/macos/requirements.txt rename to static/build_files/macos/requirementsQt5.txt diff --git a/static/build_files/macos/requirementsQt6.txt b/static/build_files/macos/requirementsQt6.txt new file mode 100644 index 00000000..af4df4b3 --- /dev/null +++ b/static/build_files/macos/requirementsQt6.txt @@ -0,0 +1,24 @@ +beautifulsoup4>=4.0.0 +cbor2 +chardet>=3.0.4 +cloudscraper>=1.2.33 +html5lib>=1.0.1 +lxml>=4.5.0 +lz4>=3.0.0 +nose>=1.3.0 +numpy>=1.16.0 +opencv-python-headless>=4.0.0, <=4.5.3.56 +Pillow>=6.0.0 +psutil>=5.0.0 +pylzma>=0.5.0 +pyOpenSSL>=19.1.0 +PySide6>=6.0.0 +PySocks>=1.7.0 +python-mpv==0.4.5 +PyYAML>=5.0.0 +QtPy>=1.9.0 +requests==2.23.0 +Send2Trash>=1.5.0 +service-identity>=18.1.0 +six>=1.14.0 +Twisted>=20.3.0 \ No newline at end of file diff --git a/static/build_files/windows/requirements.txt b/static/build_files/windows/requirementsQt5.txt similarity index 100% rename from static/build_files/windows/requirements.txt rename to static/build_files/windows/requirementsQt5.txt diff --git a/static/build_files/windows/requirementsQt6.txt b/static/build_files/windows/requirementsQt6.txt new file mode 100644 index 00000000..161ddb0b --- /dev/null +++ b/static/build_files/windows/requirementsQt6.txt @@ -0,0 +1,28 @@ +beautifulsoup4>=4.0.0 +cbor2 +chardet>=3.0.4 +cloudscraper>=1.2.33 +html5lib>=1.0.1 +lxml>=4.5.0 +lz4>=3.0.0 +nose>=1.3.0 +numpy>=1.16.0 +opencv-python-headless>=4.0.0, <=4.5.3.56 +Pillow>=6.0.0 +psutil>=5.0.0 +pylzma>=0.5.0 +pyOpenSSL>=19.1.0 +PySide6>=6.0.0 +PySocks>=1.7.0 +python-mpv==0.5.2 +PyYAML>=5.0.0 +QtPy>=1.9.0 +requests==2.23.0 +Send2Trash>=1.5.0 +service-identity>=18.1.0 +six>=1.14.0 +Twisted>=20.3.0 +PyWin32 +pypiwin32 +pywin32-ctypes +pefile