Merge branch 'master' of github.com:Chatterino/chatterino2 into feature/lua_scripting

This commit is contained in:
Mm2PL 2023-03-24 01:50:44 +01:00
commit ad56acfcd4
57 changed files with 1505 additions and 630 deletions

View file

@ -2,13 +2,18 @@
set -e set -e
# Print all commands as they are run
set -x
if [ ! -f ./bin/chatterino ] || [ ! -x ./bin/chatterino ]; then if [ ! -f ./bin/chatterino ] || [ ! -x ./bin/chatterino ]; then
echo "ERROR: No chatterino binary file found. This script must be run in the build folder, and chatterino must be built first." echo "ERROR: No chatterino binary file found. This script must be run in the build folder, and chatterino must be built first."
exit 1 exit 1
fi fi
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/opt/qt512/lib/" echo "Qt5_DIR set to: ${Qt5_DIR}"
export PATH="/opt/qt512/bin:$PATH"
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${Qt5_DIR}/lib"
export PATH="${Qt5_DIR}/bin:$PATH"
script_path=$(readlink -f "$0") script_path=$(readlink -f "$0")
script_dir=$(dirname "$script_path") script_dir=$(dirname "$script_path")
@ -25,20 +30,32 @@ echo ""
cp "$chatterino_dir"/resources/icon.png ./appdir/chatterino.png cp "$chatterino_dir"/resources/icon.png ./appdir/chatterino.png
linuxdeployqt_path="linuxdeployqt-6-x86_64.AppImage" linuxdeployqt_path="linuxdeployqt-x86_64.AppImage"
linuxdeployqt_url="https://github.com/probonopd/linuxdeployqt/releases/download/6/linuxdeployqt-6-x86_64.AppImage" linuxdeployqt_url="https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage"
if [ ! -f "$linuxdeployqt_path" ]; then if [ ! -f "$linuxdeployqt_path" ]; then
wget -nv "$linuxdeployqt_url" echo "Downloading LinuxDeployQT from $linuxdeployqt_url to $linuxdeployqt_path"
curl --location --fail --silent "$linuxdeployqt_url" -o "$linuxdeployqt_path"
chmod a+x "$linuxdeployqt_path" chmod a+x "$linuxdeployqt_path"
fi fi
if [ ! -f appimagetool-x86_64.AppImage ]; then
wget -nv "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" appimagetool_path="appimagetool-x86_64.AppImage"
chmod a+x appimagetool-x86_64.AppImage appimagetool_url="https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
if [ ! -f "$appimagetool_path" ]; then
echo "Downloading AppImageTool from $appimagetool_url to $appimagetool_path"
curl --location --fail --silent "$appimagetool_url" -o "$appimagetool_path"
chmod a+x "$appimagetool_path"
fi fi
# For some reason, the copyright file for libc was not found. We need to manually copy it from the host system
mkdir -p appdir/usr/share/doc/libc6/
cp /usr/share/doc/libc6/copyright appdir/usr/share/doc/libc6/
echo "Run LinuxDeployQT" echo "Run LinuxDeployQT"
./"$linuxdeployqt_path" \ ./"$linuxdeployqt_path" \
appdir/usr/share/applications/*.desktop \ --appimage-extract-and-run \
appdir/usr/share/applications/com.chatterino.chatterino.desktop \
-no-translations \ -no-translations \
-bundle-non-qt-libs \ -bundle-non-qt-libs \
-unsupported-allow-new-glibc -unsupported-allow-new-glibc
@ -56,7 +73,9 @@ cd "$here/usr"
exec "$here/usr/bin/chatterino" "$@"' > appdir/AppRun exec "$here/usr/bin/chatterino" "$@"' > appdir/AppRun
chmod a+x appdir/AppRun chmod a+x appdir/AppRun
./appimagetool-x86_64.AppImage appdir ./"$appimagetool_path" \
--appimage-extract-and-run \
appdir
# TODO: Create appimage in a unique directory instead maybe idk? # TODO: Create appimage in a unique directory instead maybe idk?
rm -rf appdir rm -rf appdir

View file

@ -16,7 +16,6 @@ Checks: "-*,
-cppcoreguidelines-pro-type-cstyle-cast, -cppcoreguidelines-pro-type-cstyle-cast,
-cppcoreguidelines-pro-bounds-pointer-arithmetic, -cppcoreguidelines-pro-bounds-pointer-arithmetic,
-cppcoreguidelines-pro-bounds-array-to-pointer-decay, -cppcoreguidelines-pro-bounds-array-to-pointer-decay,
-cppcoreguidelines-pro-type-member-init,
-cppcoreguidelines-owning-memory, -cppcoreguidelines-owning-memory,
-cppcoreguidelines-avoid-magic-numbers, -cppcoreguidelines-avoid-magic-numbers,
-readability-magic-numbers, -readability-magic-numbers,

View file

@ -1,13 +1,21 @@
FROM chatterino-ubuntu-20.04-build FROM chatterino-ubuntu-20.04-build
ADD .CI /src/.CI # In CI, this is set from the aqtinstall action
ENV Qt5_DIR=/opt/qt512
WORKDIR /src/build WORKDIR /src/build
# RUN apt-get install -y wget ADD .CI /src/.CI
# create appimage # Install dependencies necessary for AppImage packaging
# RUN pwd && ./../.CI/CreateAppImage.sh RUN apt-get update && apt-get -y install --no-install-recommends \
curl \
libfontconfig \
libxrender1 \
file
# package deb # package deb
RUN pwd && ./../.CI/CreateUbuntuDeb.sh RUN ./../.CI/CreateUbuntuDeb.sh
# package appimage
RUN ./../.CI/CreateAppImage.sh

View file

@ -1,8 +1,21 @@
FROM chatterino-ubuntu-22.04-build FROM chatterino-ubuntu-22.04-build
ADD .CI /src/.CI # In CI, this is set from the aqtinstall action
ENV Qt5_DIR=/opt/qt515
WORKDIR /src/build WORKDIR /src/build
ADD .CI /src/.CI
# Install dependencies necessary for AppImage packaging
RUN apt-get update && apt-get -y install --no-install-recommends \
curl \
libxcb-shape0 \
libfontconfig1 \
file
# package deb # package deb
RUN ./../.CI/CreateUbuntuDeb.sh RUN ./../.CI/CreateUbuntuDeb.sh
# package appimage
RUN ./../.CI/CreateAppImage.sh

View file

@ -7,9 +7,9 @@
To build, from the repo root To build, from the repo root
1. Build a docker image that contains all the build artifacts and source from building Chatterino on Ubuntu 20.04 1. Build a docker image that contains all the build artifacts and source from building Chatterino on Ubuntu 20.04
`docker build -t chatterino-ubuntu-20.04-build -f .docker/Dockerfile-ubuntu-20.04-build .` `docker buildx build -t chatterino-ubuntu-20.04-build -f .docker/Dockerfile-ubuntu-20.04-build .`
1. Build a docker image that uses the above-built image & packages it into a .deb file 1. Build a docker image that uses the above-built image & packages it into a .deb file
`docker build -t chatterino-ubuntu-20.04-package -f .docker/Dockerfile-ubuntu-20.04-package .` `docker buildx build -t chatterino-ubuntu-20.04-package -f .docker/Dockerfile-ubuntu-20.04-package .`
To extract the final package, you can run the following command: To extract the final package, you can run the following command:
`docker run -v $PWD:/opt/mount --rm -it chatterino-ubuntu-20.04-package bash -c "cp /src/build/Chatterino-x86_64.deb /opt/mount/"` `docker run -v $PWD:/opt/mount --rm -it chatterino-ubuntu-20.04-package bash -c "cp /src/build/Chatterino-x86_64.deb /opt/mount/"`
@ -21,9 +21,11 @@ To extract the final package, you can run the following command:
To build, from the repo root To build, from the repo root
1. Build a docker image that contains all the build artifacts and source from building Chatterino on Ubuntu 22.04 1. Build a docker image that contains all the build artifacts and source from building Chatterino on Ubuntu 22.04
`docker build -t chatterino-ubuntu-22.04-build -f .docker/Dockerfile-ubuntu-22.04-build .` `docker buildx build -t chatterino-ubuntu-22.04-build -f .docker/Dockerfile-ubuntu-22.04-build .`
1. Build a docker image that uses the above-built image & packages it into a .deb file 1. Build a docker image that uses the above-built image & packages it into a .deb file
`docker build -t chatterino-ubuntu-22.04-package -f .docker/Dockerfile-ubuntu-22.04-package .` `docker buildx build -t chatterino-ubuntu-22.04-package -f .docker/Dockerfile-ubuntu-22.04-package .`
To extract the final package, you can run the following command: To extract the final package, you can run the following command:
`docker run -v $PWD:/opt/mount --rm -it chatterino-ubuntu-22.04-package bash -c "cp /src/build/Chatterino-x86_64.deb /opt/mount/"` `docker run -v $PWD:/opt/mount --rm -it chatterino-ubuntu-22.04-package bash -c "cp /src/build/Chatterino-x86_64.deb /opt/mount/"`
NOTE: The AppImage from Ubuntu 22.04 is broken. Approach with caution

View file

@ -158,7 +158,7 @@ jobs:
- name: Install dependencies (Windows) - name: Install dependencies (Windows)
if: startsWith(matrix.os, 'windows') if: startsWith(matrix.os, 'windows')
run: | run: |
choco install conan -y choco install conan -y --version 1.58.0
- name: Enable Developer Command Prompt - name: Enable Developer Command Prompt
if: startsWith(matrix.os, 'windows') if: startsWith(matrix.os, 'windows')
@ -180,6 +180,7 @@ jobs:
-G"NMake Makefiles" ` -G"NMake Makefiles" `
-DCMAKE_BUILD_TYPE=RelWithDebInfo ` -DCMAKE_BUILD_TYPE=RelWithDebInfo `
-DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake" ` -DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake" `
-DUSE_PRECOMPILED_HEADERS=${{ matrix.pch }} `
-DBUILD_WITH_CRASHPAD="$Env:C2_ENABLE_CRASHPAD" ` -DBUILD_WITH_CRASHPAD="$Env:C2_ENABLE_CRASHPAD" `
-DCHATTERINO_LTO="$Env:C2_ENABLE_LTO" ` -DCHATTERINO_LTO="$Env:C2_ENABLE_LTO" `
-DCHATTERINO_PLUGINS="$Env:C2_PLUGINS" ` -DCHATTERINO_PLUGINS="$Env:C2_PLUGINS" `
@ -294,7 +295,7 @@ jobs:
clang-tidy-review-metadata.json clang-tidy-review-metadata.json
- name: Package - AppImage (Ubuntu) - name: Package - AppImage (Ubuntu)
if: startsWith(matrix.os, 'ubuntu') && matrix.skip_artifact != 'yes' if: startsWith(matrix.os, 'ubuntu-20.04') && matrix.skip_artifact != 'yes'
run: | run: |
cd build cd build
sh ./../.CI/CreateAppImage.sh sh ./../.CI/CreateAppImage.sh
@ -308,7 +309,7 @@ jobs:
shell: bash shell: bash
- name: Upload artifact - AppImage (Ubuntu) - name: Upload artifact - AppImage (Ubuntu)
if: startsWith(matrix.os, 'ubuntu') && matrix.skip_artifact != 'yes' if: startsWith(matrix.os, 'ubuntu-20.04') && matrix.skip_artifact != 'yes'
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: Chatterino-x86_64-${{ matrix.qt-version }}-${{ env.artifact_descr }}.AppImage name: Chatterino-x86_64-${{ matrix.qt-version }}-${{ env.artifact_descr }}.AppImage
@ -389,7 +390,7 @@ jobs:
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
with: with:
name: Chatterino-x86_64-5.15.2.AppImage name: Chatterino-x86_64-5.12.12.AppImage
path: release-artifacts/ path: release-artifacts/
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3

View file

@ -1,5 +1,5 @@
# emoji.json should remain minified # JSON resources should not be prettified
resources/emoji.json resources/*.json
# Ignore submodule files # Ignore submodule files
lib/*/ lib/*/
@ -21,3 +21,6 @@ dependencies
# vcpkg # vcpkg
vcpkg_installed/ vcpkg_installed/
# Compile commands generated by CMake
compile_commands.json

View file

@ -8,7 +8,7 @@ Note on Qt version compatibility: If you are installing Qt from a package manage
_Most likely works the same for other Debian-like distros_ _Most likely works the same for other Debian-like distros_
Install all of the dependencies using `sudo apt install qttools5-dev qt5-image-formats-plugins libqt5svg5-dev libboost-dev libssl-dev libboost-system-dev libboost-filesystem-dev cmake g++` Install all of the dependencies using `sudo apt install qttools5-dev qt5-image-formats-plugins libqt5svg5-dev libboost-dev libssl-dev libboost-system-dev libboost-filesystem-dev cmake g++ libsecret-1-dev`
### Arch Linux ### Arch Linux

View file

@ -1,19 +1,50 @@
# Building on Windows # Building on Windows
**Note that installing all of the development prerequisites and libraries will require about 30 GB of free disk space. Please ensure this space is available on your `C:` drive before proceeding.** **Note that installing all of the development prerequisites and libraries will require about 40 GB of free disk space. Please ensure this space is available on your `C:` drive before proceeding.**
This guide assumes you are on a 64-bit system. You might need to manually search out alternate download links should you desire to build Chatterino on a 32-bit system. This guide assumes you are on a 64-bit system. You might need to manually search out alternate download links should you desire to build Chatterino on a 32-bit system.
## Visual Studio 2022 ## Installing prerequisites
### Visual Studio
Download and install [Visual Studio 2022 Community](https://visualstudio.microsoft.com/downloads/). In the installer, select "Desktop development with C++" and "Universal Windows Platform development". Download and install [Visual Studio 2022 Community](https://visualstudio.microsoft.com/downloads/). In the installer, select "Desktop development with C++" and "Universal Windows Platform development".
Notes: Notes:
- This installation will take about 17 GB of disk space - This installation will take about 21 GB of disk space
- You do not need to sign in with a Microsoft account after setup completes. You may simply exit the login dialog. - You do not need to sign in with a Microsoft account after setup completes. You may simply exit the login dialog.
## Boost ### Qt
1. Visit the [Qt Open Source Page](https://www.qt.io/download-open-source).
2. Scroll down to the bottom
3. Then select "Download the Qt Online Installer"
Notes:
- Installing the latest **stable** Qt version is advised for new installations, but if you want to use your existing installation please ensure you are running **Qt 5.12 or later**.
#### When prompted which components to install:
1. Unfold the tree element that says "Qt"
2. Unfold the top most tree element (latest stable Qt version, e.g. `Qt 5.15.2`)
3. Under this version, select the following entries:
- `MSVC 2019 64-bit` (or alternative version if you are using that)
- `Qt WebEngine` (optional)
4. Under the "Tools" tree element (at the bottom), ensure that `Qt Creator X.X.X` and `Debugging Tools for Windows` are selected. (they should be checked by default)
5. Continue through the installer and let the installer finish installing Qt.
Note: This installation will take about 2 GB of disk space.
Once Qt is done installing, make sure you add its bin directory to your `PATH` (e.g. `C:\Qt\5.15.2\msvc2019_64\bin`)
### Advanced dependencies
These dependencies are only required if you are not using a package manager
<details>
<summary>Boost</summary>
1. First, download a boost installer appropriate for your version of Visual Studio. 1. First, download a boost installer appropriate for your version of Visual Studio.
@ -29,66 +60,61 @@ Notes:
Note: This installation will take about 2.1 GB of disk space. Note: This installation will take about 2.1 GB of disk space.
## OpenSSL </details>
<details>
<summary>OpenSSL</summary>
### For our websocket library, we need OpenSSL 1.1 ### For our websocket library, we need OpenSSL 1.1
1. Download OpenSSL for windows, version `1.1.1s`: **[Download](https://slproweb.com/download/Win64OpenSSL-1_1_1s.exe)** 1. Download OpenSSL for windows, version `1.1.1s`: **[Download](https://web.archive.org/web/20221101204129/https://slproweb.com/download/Win64OpenSSL-1_1_1s.exe)**
2. When prompted, install OpenSSL to `C:\local\openssl` 2. When prompted, install OpenSSL to `C:\local\openssl`
3. When prompted, copy the OpenSSL DLLs to "The OpenSSL binaries (/bin) directory". 3. When prompted, copy the OpenSSL DLLs to "The OpenSSL binaries (/bin) directory".
### For Qt SSL, we need OpenSSL 1.0
1. Download OpenSSL for Windows, version `1.0.2u`: **[Download](https://web.archive.org/web/20211109231823/https://slproweb.com/download/Win64OpenSSL-1_0_2u.exe)**
2. When prompted, install it to any arbitrary empty directory.
3. When prompted, copy the OpenSSL DLLs to "The OpenSSL binaries (/bin) directory".
4. Copy the OpenSSL 1.0 files from its `\bin` folder to `C:\local\bin` (You will need to create the folder)
5. Then copy the OpenSSL 1.1 files from its `\bin` folder to `C:\local\bin` (Overwrite any duplicate files)
6. Add `C:\local\bin` to your path folder ([Follow the guide here if you don't know how to do it](https://www.computerhope.com/issues/ch000549.htm#windows10))
**If the 1.1.x download link above does not work, try downloading the similar 1.1.x version found [here](https://slproweb.com/products/Win32OpenSSL.html). Note: Don't download the "light" installer, it does not have the required files.**
![Screenshot Slproweb layout](https://user-images.githubusercontent.com/41973452/175827529-97802939-5549-4ab1-95c4-d39f012d06e9.png)
Note: This installation will take about 200 MB of disk space. Note: This installation will take about 200 MB of disk space.
## Qt </details>
1. Visit the [Qt Open Source Page](https://www.qt.io/download-open-source). ## Building
2. Scroll down to the bottom
3. Then select "Download the Qt Online Installer"
Notes: ### Using CMake
- Installing the latest **stable** Qt version is advised for new installations, but if you want to use your existing installation please ensure you are running **Qt 5.12 or later**. #### Install conan
### When prompted which components to install: Install [conan](https://conan.io/downloads.html) and make sure it's in your `PATH` (default setting).
1. Unfold the tree element that says "Qt" Then in a terminal, configure conan to use `NMake Makefiles` as its generator:
2. Unfold the top most tree element (latest stable Qt version, e.g. `Qt 5.15.2`)
3. Under this version, select the following entries:
- `MSVC 2019 64-bit` (or alternative version if you are using that)
- `Qt WebEngine` (optional)
4. Under the "Tools" tree element (at the bottom), ensure that `Qt Creator X.X.X` and `Debugging Tools for Windows` are selected. (they should be checked by default)
5. Continue through the installer and let the installer finish installing Qt.
Note: This installation will take about 2 GB of disk space. 1. Generate a new profile
`conan profile new --detect --force default`
1. Configure the profile to use `NMake Makefiles` as its generator
`conan profile update conf.tools.cmake.cmaketoolchain:generator="NMake Makefiles" default`
## Compile with Breakpad support (Optional) #### Build
Compiling with Breakpad support enables crash reports that can be of use for developing/beta versions of Chatterino. If you have no interest in reporting crashes anyways, this optional dependency will probably be of no use to you. Open up your terminal with the Visual Studio environment variables (e.g. `x64 Native Tools Command Prompt for VS 2022`), cd to the cloned c2 directory and run the following commands:
1. Open up `lib/qBreakpad/handler/handler.pro`in Qt Creator 1. `mkdir build`
2. Build it in whichever mode you want to build Chatterino in (Debug/Profile/Release) 1. `cd build`
3. Copy the newly built `qBreakpad.lib` to the following directory: `lib/qBreakpad/build/handler` (You will have to manually create this directory) 1. `conan install .. -s build_type=Release --build=missing`
1. `cmake -G"NMake Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake" -DCMAKE_PREFIX_PATH="C:\Qt\5.15.2\msvc2019_64" ..`
1. `nmake`
## Run the build in Qt Creator #### Ensure DLLs are available
Once Chatterino has finished building, to ensure all .dll's are available you can run this from the build directory:
`windeployqt bin/chatterino.exe --release --no-compiler-runtime --no-translations --no-opengl-sw --dir bin/`
Can't find windeployqt? You forgot to add your Qt bin directory (e.g. `C:\Qt\5.15.2\msvc2019_64\bin`) to your `PATH`
### Run the build in Qt Creator
1. Open the `CMakeLists.txt` file by double-clicking it, or by opening it via Qt Creator. 1. Open the `CMakeLists.txt` file by double-clicking it, or by opening it via Qt Creator.
2. You will be presented with a screen that is titled "Configure Project". In this screen, you should have at least one option present ready to be configured, like this: 2. You will be presented with a screen that is titled "Configure Project". In this screen, you should have at least one option present ready to be configured, like this:
![Qt Create Configure Project screenshot](https://user-images.githubusercontent.com/69117321/169887645-2ae0871a-fe8a-4eb9-98db-7b996dea3a54.png) ![Qt Create Configure Project screenshot](https://user-images.githubusercontent.com/69117321/169887645-2ae0871a-fe8a-4eb9-98db-7b996dea3a54.png)
3. Select the profile(s) you want to build with and click "Configure Project". 3. Select the profile(s) you want to build with and click "Configure Project".
### How to run and produce builds #### How to run and produce builds
- In the main screen, click the green "play symbol" on the bottom left to run the project directly. - In the main screen, click the green "play symbol" on the bottom left to run the project directly.
- Click the hammer on the bottom left to generate a build (does not run the build though). - Click the hammer on the bottom left to generate a build (does not run the build though).
@ -98,7 +124,7 @@ Build results will be placed in a folder at the same level as the "chatterino2"
- Note that if you are building chatterino purely for usage, not for development, it is recommended that you click the "PC" icon above the play icon and select "Release" instead of "Debug". - Note that if you are building chatterino purely for usage, not for development, it is recommended that you click the "PC" icon above the play icon and select "Release" instead of "Debug".
- Output and error messages produced by the compiler can be seen under the "4 Compile Output" tab in Qt Creator. - Output and error messages produced by the compiler can be seen under the "4 Compile Output" tab in Qt Creator.
## Producing standalone builds #### Producing standalone builds
If you build chatterino, the result directories will contain a `chatterino.exe` file in the `$OUTPUTDIR\release\` directory. This `.exe` file will not directly run on any given target system, because it will be lacking various Qt runtimes. If you build chatterino, the result directories will contain a `chatterino.exe` file in the `$OUTPUTDIR\release\` directory. This `.exe` file will not directly run on any given target system, because it will be lacking various Qt runtimes.
@ -114,27 +140,11 @@ To produce all supplement files for a standalone build, follow these steps (adju
cd C:\Users\example\src\build-chatterino-Desktop_Qt_5_15_2_MSVC2019_64bit-Release\release cd C:\Users\example\src\build-chatterino-Desktop_Qt_5_15_2_MSVC2019_64bit-Release\release
C:\Qt\5.15.2\msvc2019_64\bin\windeployqt.exe chatterino.exe C:\Qt\5.15.2\msvc2019_64\bin\windeployqt.exe chatterino.exe
5. Go to `C:\local\bin\` and copy these dll's into your `release folder`. 5. The `releases` directory will now be populated with all the required files to make the chatterino build standalone.
libssl-1_1-x64.dll
libcrypto-1_1-x64.dll
ssleay32.dll
libeay32.dll
6. The `releases` directory will now be populated with all the required files to make the chatterino build standalone.
You can now create a zip archive of all the contents in `releases` and distribute the program as is, without requiring any development tools to be present on the target system. (However, the vcredist package must be present, as usual - see the [README](README.md)). You can now create a zip archive of all the contents in `releases` and distribute the program as is, without requiring any development tools to be present on the target system. (However, the vcredist package must be present, as usual - see the [README](README.md)).
## Using CMake ### Building on MSVC with AddressSanitizer
Open up your terminal with the Visual Studio environment variables, then enter the following commands:
1. `mkdir build`
2. `cd build`
3. `cmake -G"NMake Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake" ..`
4. `nmake`
## Building on MSVC with AddressSanitizer
Make sure you installed `C++ AddressSanitizer` in your VisualStudio installation like described in the [Microsoft Docs](https://learn.microsoft.com/en-us/cpp/sanitizers/asan#install-the-addresssanitizer). Make sure you installed `C++ AddressSanitizer` in your VisualStudio installation like described in the [Microsoft Docs](https://learn.microsoft.com/en-us/cpp/sanitizers/asan#install-the-addresssanitizer).
@ -145,7 +155,7 @@ copy the file found in `<VisualStudio-installation-path>\VC\Tools\MSVC\<version>
To learn more about AddressSanitizer and MSVC, visit the [Microsoft Docs](https://learn.microsoft.com/en-us/cpp/sanitizers/asan). To learn more about AddressSanitizer and MSVC, visit the [Microsoft Docs](https://learn.microsoft.com/en-us/cpp/sanitizers/asan).
## Building/Running in CLion ### Building/Running in CLion
_Note:_ We're using `build` instead of the CLion default `cmake-build-debug` folder. _Note:_ We're using `build` instead of the CLion default `cmake-build-debug` folder.
@ -200,7 +210,7 @@ Now you can run the `chatterino | Debug` configuration.
If you want to run the portable version of Chatterino, create a file called `modes` inside of `build/bin` and If you want to run the portable version of Chatterino, create a file called `modes` inside of `build/bin` and
write `portable` into it. write `portable` into it.
### Debugging #### Debugging
To visualize QT types like `QString`, you need to inform CLion and LLDB To visualize QT types like `QString`, you need to inform CLion and LLDB
about these types. about these types.

View file

@ -1,28 +1,38 @@
# Building on Windows with vcpkg # Building on Windows with vcpkg
This will require more than 30GB of free space on your hard drive.
## Prerequisites ## Prerequisites
1. Install [Visual Studio](https://visualstudio.microsoft.com/) with "Desktop development with C++" (~9.66 GB) 1. Install [Visual Studio](https://visualstudio.microsoft.com/) with "Desktop development with C++"
1. Install [CMake](https://cmake.org/) (~109 MB) 1. Install [CMake](https://cmake.org/)
1. Install [git](https://git-scm.com/) (~264 MB) 1. Install [git](https://git-scm.com/)
1. Install [vcpkg](https://vcpkg.io/) (~80 MB) 1. Install [vcpkg](https://vcpkg.io/)
- `git clone https://github.com/Microsoft/vcpkg.git` - `git clone https://github.com/Microsoft/vcpkg.git`
- `cd .\vcpkg\` - `cd .\vcpkg\`
- `.\bootstrap-vcpkg.bat` - `.\bootstrap-vcpkg.bat`
- `.\vcpkg integrate install` - `.\vcpkg integrate install`
- `.\vcpkg integrate powershell` - `.\vcpkg integrate powershell`
- `cd ..` - `cd ..`
1. Configure the environment for vcpkg 1. Configure the environment variables for vcpkg.
- `set VCPKG_DEFAULT_TRIPLET=x64-windows` Check [this document](https://gist.github.com/mitchmindtree/92c8e37fa80c8dddee5b94fc88d1288b#setting-an-environment-variable-on-windows) for more information for how to set environment variables on Windows.
- [default](https://github.com/microsoft/vcpkg/blob/master/docs/users/triplets.md#additional-remarks) is `x86-windows` - Ensure your dependencies are built as 64-bit
- `set VCPKG_ROOT=C:\path\to\vcpkg\` e.g. `setx VCPKG_DEFAULT_TRIPLET x64-windows`
- `set PATH=%PATH%;%VCPKG_ROOT%` See [documentation about Triplets](https://learn.microsoft.com/en-gb/vcpkg/users/triplets)
[default](https://github.com/microsoft/vcpkg/blob/master/docs/users/triplets.md#additional-remarks) is `x86-windows`
- Set VCPKG_ROOT to the vcpkg path
e.g. `setx VCPKG_ROOT <path to vcpkg>`
See [VCPKG_ROOT documentation](https://learn.microsoft.com/en-gb/vcpkg/users/config-environment#vcpkg_root)
- Append the vcpkg path to your path
e.g. `setx PATH "%PATH%;<path to vcpkg>"`
- For more configurations, see https://learn.microsoft.com/en-gb/vcpkg/users/config-environment
1. You may need to restart your computer to ensure all your environment variables and what-not are loaded everywhere.
## Building ## Building
1. Clone 1. Clone
- `git clone --recurse-submodules https://github.com/Chatterino/chatterino2.git` - `git clone --recurse-submodules https://github.com/Chatterino/chatterino2.git`
1. Install dependencies (~21 GB) 1. Install dependencies
- `cd .\chatterino2\` - `cd .\chatterino2\`
- `vcpkg install` - `vcpkg install`
1. Build 1. Build

View file

@ -2,9 +2,27 @@
## Unversioned ## Unversioned
- Minor: Delete all but the last 5 crashdumps on application start. (#4392) - Minor: Include normally-stripped mention in replies in logs. (#4420)
- Dev: Add capability to build Chatterino with Qt6. (#4393) - Minor: Added support for FrankerFaceZ animated emotes. (#4434)
- Dev: Fix homebrew update action. (#4394) - Minor: Added a local backup of the Twitch Badges API in case the request fails. (#4463)
- Bugfix: Fixed an issue where animated emotes would render on top of zero-width emotes. (#4314)
- Bugfix: Fixed an issue where it was difficult to hover a zero-width emote. (#4314)
- Bugfix: Fixed an issue where context-menu items for zero-width emotes displayed the wrong provider. (#4460)
- Bugfix: Fixed an issue where the "Enable zero-width emotes" setting was showing the inverse state. (#4462)
- Dev: Disabling precompiled headers on Windows is now tested in CI. (#4472)
- Dev: Ignore unhandled BTTV user-events. (#4438)
- Dev: Only log debug messages when NDEBUG is not defined. (#4442)
- Dev: Cleaned up theme related code. (#4450)
## 2.4.2
- Minor: Added `/banid` command that allows banning by user ID. (#4411)
- Bugfix: Fixed FrankerFaceZ emotes/badges not loading due to an API change. (#4432)
- Bugfix: Fixed uploaded AppImage not being able to execute most web requests. (#4400)
- Bugfix: Fixed a potential race condition due to using the wrong lock when loading 7TV badges. (#4402)
- Dev: Delete all but the last 5 crashdumps on application start. (#4392)
- Dev: Added capability to build Chatterino with Qt6. (#4393)
- Dev: Fixed homebrew update action. (#4394)
## 2.4.1 ## 2.4.1

View file

@ -8,7 +8,7 @@ list(APPEND CMAKE_MODULE_PATH
"${CMAKE_SOURCE_DIR}/cmake/sanitizers-cmake/cmake" "${CMAKE_SOURCE_DIR}/cmake/sanitizers-cmake/cmake"
) )
project(chatterino VERSION 2.4.1) project(chatterino VERSION 2.4.2)
option(BUILD_APP "Build Chatterino" ON) option(BUILD_APP "Build Chatterino" ON)
option(BUILD_TESTS "Build the tests for Chatterino" OFF) option(BUILD_TESTS "Build the tests for Chatterino" OFF)

@ -1 +1 @@
Subproject commit 7d37cbfd5ac3bfbe046118e1cec3d32ba4696469 Subproject commit 1f99aa808eda5e717245254032c6bf58b0fc088a

View file

@ -32,6 +32,6 @@
<binary>chatterino</binary> <binary>chatterino</binary>
</provides> </provides>
<releases> <releases>
<release version="2.4.1" date="2023-02-18"/> <release version="2.4.2" date="2023-03-05"/>
</releases> </releases>
</component> </component>

View file

@ -57,6 +57,7 @@ Jaxkey | https://github.com/Jaxkey | :/avatars/jaxkey.png | Contributor
Explooosion | https://github.com/Explooosion-code | :/avatars/explooosion_code.png | Contributor Explooosion | https://github.com/Explooosion-code | :/avatars/explooosion_code.png | Contributor
mohad12211 | https://github.com/mohad12211 | :/avatars/mohad12211.png | Contributor mohad12211 | https://github.com/mohad12211 | :/avatars/mohad12211.png | Contributor
Wissididom | https://github.com/Wissididom | :/avatars/wissididom.png | Contributor Wissididom | https://github.com/Wissididom | :/avatars/wissididom.png | Contributor
03y | https://github.com/03y | | Contributor
# If you are a contributor add yourself above this line # If you are a contributor add yourself above this line

File diff suppressed because one or more lines are too long

View file

@ -234,6 +234,8 @@ set(SOURCE_FILES
providers/ffz/FfzBadges.hpp providers/ffz/FfzBadges.hpp
providers/ffz/FfzEmotes.cpp providers/ffz/FfzEmotes.cpp
providers/ffz/FfzEmotes.hpp providers/ffz/FfzEmotes.hpp
providers/ffz/FfzUtil.cpp
providers/ffz/FfzUtil.hpp
providers/irc/AbstractIrcServer.cpp providers/irc/AbstractIrcServer.cpp
providers/irc/AbstractIrcServer.hpp providers/irc/AbstractIrcServer.hpp
@ -427,6 +429,8 @@ set(SOURCE_FILES
widgets/Scrollbar.hpp widgets/Scrollbar.hpp
widgets/StreamView.cpp widgets/StreamView.cpp
widgets/StreamView.hpp widgets/StreamView.hpp
widgets/TooltipEntryWidget.cpp
widgets/TooltipEntryWidget.hpp
widgets/TooltipWidget.cpp widgets/TooltipWidget.cpp
widgets/TooltipWidget.hpp widgets/TooltipWidget.hpp
widgets/Window.cpp widgets/Window.cpp

View file

@ -42,6 +42,12 @@ public:
reinterpret_cast<Q &>(this->value_) |= static_cast<Q>(flag); reinterpret_cast<Q &>(this->value_) |= static_cast<Q>(flag);
} }
/** Adds the flags from `flags` in this enum. */
void set(FlagsEnum flags)
{
reinterpret_cast<Q &>(this->value_) |= static_cast<Q>(flags.value_);
}
void unset(T flag) void unset(T flag)
{ {
reinterpret_cast<Q &>(this->value_) &= ~static_cast<Q>(flag); reinterpret_cast<Q &>(this->value_) &= ~static_cast<Q>(flag);
@ -69,6 +75,12 @@ public:
return xd; return xd;
} }
FlagsEnum operator|(FlagsEnum rhs)
{
return static_cast<T>(static_cast<Q>(this->value_) |
static_cast<Q>(rhs.value_));
}
bool hasAny(FlagsEnum flags) const bool hasAny(FlagsEnum flags) const
{ {
return static_cast<Q>(this->value_) & static_cast<Q>(flags.value_); return static_cast<Q>(this->value_) & static_cast<Q>(flags.value_);

View file

@ -1,6 +1,6 @@
#include "common/QLogging.hpp" #include "common/QLogging.hpp"
#ifdef DEBUG_OFF #ifdef NDEBUG
static constexpr QtMsgType logThreshold = QtWarningMsg; static constexpr QtMsgType logThreshold = QtWarningMsg;
#else #else
static constexpr QtMsgType logThreshold = QtDebugMsg; static constexpr QtMsgType logThreshold = QtDebugMsg;

View file

@ -24,7 +24,7 @@
* - 2.4.0-alpha.2 * - 2.4.0-alpha.2
* - 2.4.0-alpha * - 2.4.0-alpha
**/ **/
#define CHATTERINO_VERSION "2.4.1" #define CHATTERINO_VERSION "2.4.2"
#if defined(Q_OS_WIN) #if defined(Q_OS_WIN)
# define CHATTERINO_OS "win" # define CHATTERINO_OS "win"

View file

@ -2633,7 +2633,7 @@ void CommandController::initialize(Settings &, Paths &paths)
auto formatBanTimeoutError = auto formatBanTimeoutError =
[](const char *operation, HelixBanUserError error, [](const char *operation, HelixBanUserError error,
const QString &message, const QString &userDisplayName) -> QString { const QString &message, const QString &userTarget) -> QString {
using Error = HelixBanUserError; using Error = HelixBanUserError;
QString errorMessage = QString("Failed to %1 user - ").arg(operation); QString errorMessage = QString("Failed to %1 user - ").arg(operation);
@ -2660,7 +2660,7 @@ void CommandController::initialize(Settings &, Paths &paths)
case Error::TargetBanned: { case Error::TargetBanned: {
// Equivalent IRC error // Equivalent IRC error
errorMessage += QString("%1 is already banned in this channel.") errorMessage += QString("%1 is already banned in this channel.")
.arg(userDisplayName); .arg(userTarget);
} }
break; break;
@ -2670,8 +2670,8 @@ void CommandController::initialize(Settings &, Paths &paths)
// The messages from IRC are formatted like this: // The messages from IRC are formatted like this:
// "You cannot {op} moderator {mod} unless you are the owner of this channel." // "You cannot {op} moderator {mod} unless you are the owner of this channel."
// "You cannot {op} the broadcaster." // "You cannot {op} the broadcaster."
errorMessage += QString("You cannot %1 %2.") errorMessage +=
.arg(operation, userDisplayName); QString("You cannot %1 %2.").arg(operation, userTarget);
} }
break; break;
@ -2830,6 +2830,53 @@ void CommandController::initialize(Settings &, Paths &paths)
return ""; return "";
}); });
this->registerCommand("/banid", [formatBanTimeoutError](
const QStringList &words,
auto channel) {
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
if (twitchChannel == nullptr)
{
channel->addMessage(makeSystemMessage(
QString("The /banid command only works in Twitch channels")));
return "";
}
const auto *usageStr =
"Usage: \"/banid <userID> [reason]\" - Permanently prevent a user "
"from chatting via their user ID. Reason is optional and will be "
"shown to the target user and other moderators.";
if (words.size() < 2)
{
channel->addMessage(makeSystemMessage(usageStr));
return "";
}
auto currentUser = getApp()->accounts->twitch.getCurrent();
if (currentUser->isAnon())
{
channel->addMessage(
makeSystemMessage("You must be logged in to ban someone!"));
return "";
}
auto target = words.at(1);
auto reason = words.mid(2).join(' ');
getHelix()->banUser(
twitchChannel->roomId(), currentUser->getUserId(), target,
boost::none, reason,
[] {
// No response for bans, they're emitted over pubsub/IRC instead
},
[channel, target, formatBanTimeoutError](auto error, auto message) {
auto errorMessage =
formatBanTimeoutError("ban", error, message, "#" + target);
channel->addMessage(makeSystemMessage(errorMessage));
});
return "";
});
for (const auto &cmd : TWITCH_WHISPER_COMMANDS) for (const auto &cmd : TWITCH_WHISPER_COMMANDS)
{ {
this->registerCommand(cmd, [](const QStringList &words, auto channel) { this->registerCommand(cmd, [](const QStringList &words, auto channel) {

View file

@ -687,6 +687,26 @@ void MessageBuilder::append(std::unique_ptr<MessageElement> element)
this->message().elements.push_back(std::move(element)); this->message().elements.push_back(std::move(element));
} }
bool MessageBuilder::isEmpty() const
{
return this->message_->elements.empty();
}
MessageElement &MessageBuilder::back()
{
assert(!this->isEmpty());
return *this->message().elements.back();
}
std::unique_ptr<MessageElement> MessageBuilder::releaseBack()
{
assert(!this->isEmpty());
auto ptr = std::move(this->message().elements.back());
this->message().elements.pop_back();
return ptr;
}
QString MessageBuilder::matchLink(const QString &string) QString MessageBuilder::matchLink(const QString &string)
{ {
LinkParser linkParser(string); LinkParser linkParser(string);

View file

@ -123,6 +123,10 @@ protected:
virtual void addTextOrEmoji(EmotePtr emote); virtual void addTextOrEmoji(EmotePtr emote);
virtual void addTextOrEmoji(const QString &value); virtual void addTextOrEmoji(const QString &value);
bool isEmpty() const;
MessageElement &back();
std::unique_ptr<MessageElement> releaseBack();
MessageColor textColor_ = MessageColor::Text; MessageColor textColor_ = MessageColor::Text;
private: private:

View file

@ -15,6 +15,24 @@
namespace chatterino { namespace chatterino {
namespace {
// Computes the bounding box for the given vector of images
QSize getBoundingBoxSize(const std::vector<ImagePtr> &images)
{
int width = 0;
int height = 0;
for (const auto &img : images)
{
width = std::max(width, img->width());
height = std::max(height, img->height());
}
return QSize(width, height);
}
} // namespace
MessageElement::MessageElement(MessageElementFlags flags) MessageElement::MessageElement(MessageElementFlags flags)
: flags_(flags) : flags_(flags)
{ {
@ -92,6 +110,11 @@ MessageElementFlags MessageElement::getFlags() const
return this->flags_; return this->flags_;
} }
void MessageElement::addFlags(MessageElementFlags flags)
{
this->flags_.set(flags);
}
MessageElement *MessageElement::updateLink() MessageElement *MessageElement::updateLink()
{ {
this->linkChanged.invoke(); this->linkChanged.invoke();
@ -216,6 +239,170 @@ MessageLayoutElement *EmoteElement::makeImageLayoutElement(
return new ImageLayoutElement(*this, image, size); return new ImageLayoutElement(*this, image, size);
} }
LayeredEmoteElement::LayeredEmoteElement(
std::vector<LayeredEmoteElement::Emote> &&emotes, MessageElementFlags flags,
const MessageColor &textElementColor)
: MessageElement(flags)
, emotes_(std::move(emotes))
, textElementColor_(textElementColor)
{
this->updateTooltips();
}
void LayeredEmoteElement::addEmoteLayer(const LayeredEmoteElement::Emote &emote)
{
this->emotes_.push_back(emote);
this->updateTooltips();
}
void LayeredEmoteElement::addToContainer(MessageLayoutContainer &container,
MessageElementFlags flags)
{
if (flags.hasAny(this->getFlags()))
{
if (flags.has(MessageElementFlag::EmoteImages))
{
auto images = this->getLoadedImages(container.getScale());
if (images.empty())
{
return;
}
auto emoteScale = getSettings()->emoteScale.getValue();
float overallScale = emoteScale * container.getScale();
auto largestSize = getBoundingBoxSize(images) * overallScale;
std::vector<QSize> individualSizes;
individualSizes.reserve(this->emotes_.size());
for (auto img : images)
{
individualSizes.push_back(QSize(img->width(), img->height()) *
overallScale);
}
container.addElement(this->makeImageLayoutElement(
images, individualSizes, largestSize)
->setLink(this->getLink()));
}
else
{
if (this->textElement_)
{
this->textElement_->addToContainer(container,
MessageElementFlag::Misc);
}
}
}
}
std::vector<ImagePtr> LayeredEmoteElement::getLoadedImages(float scale)
{
std::vector<ImagePtr> res;
res.reserve(this->emotes_.size());
for (const auto &emote : this->emotes_)
{
auto image = emote.ptr->images.getImageOrLoaded(scale);
if (image->isEmpty())
{
continue;
}
res.push_back(image);
}
return res;
}
MessageLayoutElement *LayeredEmoteElement::makeImageLayoutElement(
const std::vector<ImagePtr> &images, const std::vector<QSize> &sizes,
QSize largestSize)
{
return new LayeredImageLayoutElement(*this, images, sizes, largestSize);
}
void LayeredEmoteElement::updateTooltips()
{
if (!this->emotes_.empty())
{
QString copyStr = this->getCopyString();
this->textElement_.reset(new TextElement(
copyStr, MessageElementFlag::Misc, this->textElementColor_));
this->setTooltip(copyStr);
}
std::vector<QString> result;
result.reserve(this->emotes_.size());
for (const auto &emote : this->emotes_)
{
result.push_back(emote.ptr->tooltip.string);
}
this->emoteTooltips_ = std::move(result);
}
const std::vector<QString> &LayeredEmoteElement::getEmoteTooltips() const
{
return this->emoteTooltips_;
}
QString LayeredEmoteElement::getCleanCopyString() const
{
QString result;
for (size_t i = 0; i < this->emotes_.size(); ++i)
{
if (i != 0)
{
result += " ";
}
result += TwitchEmotes::cleanUpEmoteCode(
this->emotes_[i].ptr->getCopyString());
}
return result;
}
QString LayeredEmoteElement::getCopyString() const
{
QString result;
for (size_t i = 0; i < this->emotes_.size(); ++i)
{
if (i != 0)
{
result += " ";
}
result += this->emotes_[i].ptr->getCopyString();
}
return result;
}
const std::vector<LayeredEmoteElement::Emote> &LayeredEmoteElement::getEmotes()
const
{
return this->emotes_;
}
std::vector<LayeredEmoteElement::Emote> LayeredEmoteElement::getUniqueEmotes()
const
{
// Functor for std::copy_if that keeps track of seen elements
struct NotDuplicate {
bool operator()(const Emote &element)
{
return seen.insert(element.ptr).second;
}
private:
std::set<EmotePtr> seen;
};
// Get unique emotes while maintaining relative layering order
NotDuplicate dup;
std::vector<Emote> unique;
std::copy_if(this->emotes_.begin(), this->emotes_.end(),
std::back_insert_iterator(unique), dup);
return unique;
}
// BADGE // BADGE
BadgeElement::BadgeElement(const EmotePtr &emote, MessageElementFlags flags) BadgeElement::BadgeElement(const EmotePtr &emote, MessageElementFlags flags)
: MessageElement(flags) : MessageElement(flags)

View file

@ -141,9 +141,7 @@ enum class MessageElementFlag : int64_t {
LowercaseLink = (1LL << 29), LowercaseLink = (1LL << 29),
OriginalLink = (1LL << 30), OriginalLink = (1LL << 30),
// ZeroWidthEmotes are emotes that are supposed to overlay over any pre-existing emotes // Unused: (1LL << 31)
// e.g. BTTV's SoSnowy during christmas season or 7TV's RainTime
ZeroWidthEmote = (1LL << 31),
// for elements of the message reply // for elements of the message reply
RepliedMessage = (1LL << 32), RepliedMessage = (1LL << 32),
@ -189,6 +187,7 @@ public:
const Link &getLink() const; const Link &getLink() const;
bool hasTrailingSpace() const; bool hasTrailingSpace() const;
MessageElementFlags getFlags() const; MessageElementFlags getFlags() const;
void addFlags(MessageElementFlags flags);
MessageElement *updateLink(); MessageElement *updateLink();
virtual void addToContainer(MessageLayoutContainer &container, virtual void addToContainer(MessageLayoutContainer &container,
@ -321,6 +320,48 @@ private:
EmotePtr emote_; EmotePtr emote_;
}; };
// A LayeredEmoteElement represents multiple Emotes layered on top of each other.
// This class takes care of rendering animated and non-animated emotes in the
// correct order and aligning them in the right way.
class LayeredEmoteElement : public MessageElement
{
public:
struct Emote {
EmotePtr ptr;
MessageElementFlags flags;
};
LayeredEmoteElement(
std::vector<Emote> &&emotes, MessageElementFlags flags,
const MessageColor &textElementColor = MessageColor::Text);
void addEmoteLayer(const Emote &emote);
void addToContainer(MessageLayoutContainer &container,
MessageElementFlags flags) override;
// Returns a concatenation of each emote layer's cleaned copy string
QString getCleanCopyString() const;
const std::vector<Emote> &getEmotes() const;
std::vector<Emote> getUniqueEmotes() const;
const std::vector<QString> &getEmoteTooltips() const;
private:
MessageLayoutElement *makeImageLayoutElement(
const std::vector<ImagePtr> &image, const std::vector<QSize> &sizes,
QSize largestSize);
QString getCopyString() const;
void updateTooltips();
std::vector<ImagePtr> getLoadedImages(float scale);
std::vector<Emote> emotes_;
std::vector<QString> emoteTooltips_;
std::unique_ptr<TextElement> textElement_;
MessageColor textElementColor_;
};
class BadgeElement : public MessageElement class BadgeElement : public MessageElement
{ {
public: public:

View file

@ -282,10 +282,9 @@ void MessageLayout::paint(QPainter &painter, int width, int y, int messageIndex,
} }
else else
{ {
color = color = isWindowFocused
isWindowFocused ? app->themes->tabs.selected.backgrounds.regular
? app->themes->tabs.selected.backgrounds.regular.color() : app->themes->tabs.selected.backgrounds.unfocused;
: app->themes->tabs.selected.backgrounds.unfocused.color();
} }
QBrush brush(color, static_cast<Qt::BrushStyle>( QBrush brush(color, static_cast<Qt::BrushStyle>(

View file

@ -67,10 +67,7 @@ void MessageLayoutContainer::clear()
void MessageLayoutContainer::addElement(MessageLayoutElement *element) void MessageLayoutContainer::addElement(MessageLayoutElement *element)
{ {
bool isZeroWidth = if (!this->fitsInLine(element->getRect().width()))
element->getFlags().has(MessageElementFlag::ZeroWidthEmote);
if (!isZeroWidth && !this->fitsInLine(element->getRect().width()))
{ {
this->breakLine(); this->breakLine();
} }
@ -175,14 +172,6 @@ void MessageLayoutContainer::_addElement(MessageLayoutElement *element,
this->lineHeight_ = std::max(this->lineHeight_, elementLineHeight); this->lineHeight_ = std::max(this->lineHeight_, elementLineHeight);
auto xOffset = 0; auto xOffset = 0;
bool isZeroWidthEmote = element->getCreator().getFlags().has(
MessageElementFlag::ZeroWidthEmote);
if (isZeroWidthEmote && !isRTLMode)
{
xOffset -= element->getRect().width() + this->spaceWidth_;
}
auto yOffset = 0; auto yOffset = 0;
if (element->getCreator().getFlags().has( if (element->getCreator().getFlags().has(
@ -195,7 +184,7 @@ void MessageLayoutContainer::_addElement(MessageLayoutElement *element,
if (getSettings()->removeSpacesBetweenEmotes && if (getSettings()->removeSpacesBetweenEmotes &&
element->getFlags().hasAny({MessageElementFlag::EmoteImages}) && element->getFlags().hasAny({MessageElementFlag::EmoteImages}) &&
!isZeroWidthEmote && shouldRemoveSpaceBetweenEmotes()) shouldRemoveSpaceBetweenEmotes())
{ {
// Move cursor one 'space width' to the left (right in case of RTL) to combine hug the previous emote // Move cursor one 'space width' to the left (right in case of RTL) to combine hug the previous emote
if (isRTLMode) if (isRTLMode)
@ -230,8 +219,6 @@ void MessageLayoutContainer::_addElement(MessageLayoutElement *element,
} }
// set current x // set current x
if (!isZeroWidthEmote)
{
if (isRTLMode) if (isRTLMode)
{ {
this->currentX_ -= element->getRect().width(); this->currentX_ -= element->getRect().width();
@ -240,7 +227,6 @@ void MessageLayoutContainer::_addElement(MessageLayoutElement *element,
{ {
this->currentX_ += element->getRect().width(); this->currentX_ += element->getRect().width();
} }
}
if (element->hasTrailingSpace()) if (element->hasTrailingSpace())
{ {

View file

@ -15,6 +15,14 @@
namespace { namespace {
const QChar RTL_EMBED(0x202B); const QChar RTL_EMBED(0x202B);
void alignRectBottomCenter(QRectF &rect, const QRectF &reference)
{
QPointF newCenter(reference.center().x(),
reference.bottom() - (rect.height() / 2.0));
rect.moveCenter(newCenter);
}
} // namespace } // namespace
namespace chatterino { namespace chatterino {
@ -184,6 +192,133 @@ int ImageLayoutElement::getXFromIndex(int index)
} }
} }
//
// LAYERED IMAGE
//
LayeredImageLayoutElement::LayeredImageLayoutElement(
MessageElement &creator, std::vector<ImagePtr> images,
std::vector<QSize> sizes, QSize largestSize)
: MessageLayoutElement(creator, largestSize)
, images_(std::move(images))
, sizes_(std::move(sizes))
{
assert(this->images_.size() == this->sizes_.size());
this->trailingSpace = creator.hasTrailingSpace();
}
void LayeredImageLayoutElement::addCopyTextToString(QString &str, uint32_t from,
uint32_t to) const
{
const auto *layeredEmoteElement =
dynamic_cast<LayeredEmoteElement *>(&this->getCreator());
if (layeredEmoteElement)
{
// cleaning is taken care in call
str += layeredEmoteElement->getCleanCopyString();
if (this->hasTrailingSpace())
{
str += " ";
}
}
}
int LayeredImageLayoutElement::getSelectionIndexCount() const
{
return this->trailingSpace ? 2 : 1;
}
void LayeredImageLayoutElement::paint(QPainter &painter)
{
auto fullRect = QRectF(this->getRect());
for (size_t i = 0; i < this->images_.size(); ++i)
{
auto &img = this->images_[i];
if (img == nullptr)
{
continue;
}
auto pixmap = img->pixmapOrLoad();
if (img->animated())
{
// As soon as we see an animated emote layer, we can stop rendering
// the static emotes. The paintAnimated function will render any
// static emotes layered on top of the first seen animated emote.
return;
}
if (pixmap)
{
// Matching the web chat behavior, we center the emote within the overall
// binding box. E.g. small overlay emotes like cvMask will sit in the direct
// center of even wide emotes.
auto &size = this->sizes_[i];
QRectF destRect(0, 0, size.width(), size.height());
alignRectBottomCenter(destRect, fullRect);
painter.drawPixmap(destRect, *pixmap, QRectF());
}
}
}
void LayeredImageLayoutElement::paintAnimated(QPainter &painter, int yOffset)
{
auto fullRect = QRectF(this->getRect());
fullRect.moveTop(fullRect.y() + yOffset);
bool animatedFlag = false;
for (size_t i = 0; i < this->images_.size(); ++i)
{
auto &img = this->images_[i];
if (img == nullptr)
{
continue;
}
// If we have a static emote layered on top of an animated emote, we need
// to render the static emote again after animating anything below it.
if (img->animated() || animatedFlag)
{
if (auto pixmap = img->pixmapOrLoad())
{
// Matching the web chat behavior, we center the emote within the overall
// binding box. E.g. small overlay emotes like cvMask will sit in the direct
// center of even wide emotes.
auto &size = this->sizes_[i];
QRectF destRect(0, 0, size.width(), size.height());
alignRectBottomCenter(destRect, fullRect);
painter.drawPixmap(destRect, *pixmap, QRectF());
animatedFlag = true;
}
}
}
}
int LayeredImageLayoutElement::getMouseOverIndex(const QPoint &abs) const
{
return 0;
}
int LayeredImageLayoutElement::getXFromIndex(int index)
{
if (index <= 0)
{
return this->getRect().left();
}
else if (index == 1)
{
// fourtf: remove space width
return this->getRect().right();
}
else
{
return this->getRect().right();
}
}
// //
// IMAGE WITH BACKGROUND // IMAGE WITH BACKGROUND
// //

View file

@ -83,6 +83,26 @@ protected:
ImagePtr image_; ImagePtr image_;
}; };
class LayeredImageLayoutElement : public MessageLayoutElement
{
public:
LayeredImageLayoutElement(MessageElement &creator,
std::vector<ImagePtr> images,
std::vector<QSize> sizes, QSize largestSize);
protected:
void addCopyTextToString(QString &str, uint32_t from = 0,
uint32_t to = UINT32_MAX) const override;
int getSelectionIndexCount() const override;
void paint(QPainter &painter) override;
void paintAnimated(QPainter &painter, int yOffset) override;
int getMouseOverIndex(const QPoint &abs) const override;
int getXFromIndex(int index) override;
std::vector<ImagePtr> images_;
std::vector<QSize> sizes_;
};
class ImageWithBackgroundLayoutElement : public ImageLayoutElement class ImageWithBackgroundLayoutElement : public ImageLayoutElement
{ {
public: public:

View file

@ -83,6 +83,10 @@ void BttvLiveUpdates::onMessage(
this->signals_.emoteRemoved.invoke(message); this->signals_.emoteRemoved.invoke(message);
} }
else if (eventType == "lookup_user")
{
// ignored
}
else else
{ {
qCDebug(chatterinoBttv) << "Unhandled event:" << json; qCDebug(chatterinoBttv) << "Unhandled event:" << json;

View file

@ -4,6 +4,7 @@
#include "common/NetworkResult.hpp" #include "common/NetworkResult.hpp"
#include "common/Outcome.hpp" #include "common/Outcome.hpp"
#include "messages/Emote.hpp" #include "messages/Emote.hpp"
#include "providers/ffz/FfzUtil.hpp"
#include <QJsonArray> #include <QJsonArray>
#include <QJsonObject> #include <QJsonObject>
@ -67,13 +68,11 @@ void FfzBadges::load()
auto jsonBadge = jsonBadge_.toObject(); auto jsonBadge = jsonBadge_.toObject();
auto jsonUrls = jsonBadge.value("urls").toObject(); auto jsonUrls = jsonBadge.value("urls").toObject();
auto emote = Emote{ auto emote =
EmoteName{}, Emote{EmoteName{},
ImageSet{ ImageSet{parseFfzUrl(jsonUrls.value("1").toString()),
Url{QString("https:") + jsonUrls.value("1").toString()}, parseFfzUrl(jsonUrls.value("2").toString()),
Url{QString("https:") + jsonUrls.value("2").toString()}, parseFfzUrl(jsonUrls.value("4").toString())},
Url{QString("https:") +
jsonUrls.value("4").toString()}},
Tooltip{jsonBadge.value("title").toString()}, Url{}}; Tooltip{jsonBadge.value("title").toString()}, Url{}};
Badge badge; Badge badge;

View file

@ -7,6 +7,7 @@
#include "messages/Emote.hpp" #include "messages/Emote.hpp"
#include "messages/Image.hpp" #include "messages/Image.hpp"
#include "messages/MessageBuilder.hpp" #include "messages/MessageBuilder.hpp"
#include "providers/ffz/FfzUtil.hpp"
#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchChannel.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
@ -18,7 +19,7 @@ namespace {
Url getEmoteLink(const QJsonObject &urls, const QString &emoteScale) Url getEmoteLink(const QJsonObject &urls, const QString &emoteScale)
{ {
auto emote = urls.value(emoteScale); auto emote = urls[emoteScale];
if (emote.isUndefined() || emote.isNull()) if (emote.isUndefined() || emote.isNull())
{ {
return {""}; return {""};
@ -26,8 +27,9 @@ namespace {
assert(emote.isString()); assert(emote.isString());
return {"https:" + emote.toString()}; return parseFfzUrl(emote.toString());
} }
void fillInEmoteData(const QJsonObject &urls, const EmoteName &name, void fillInEmoteData(const QJsonObject &urls, const EmoteName &name,
const QString &tooltip, Emote &emoteData) const QString &tooltip, Emote &emoteData)
{ {
@ -45,6 +47,7 @@ namespace {
: Image::fromUrl(url3x, 0.25)}; : Image::fromUrl(url3x, 0.25)};
emoteData.tooltip = {tooltip}; emoteData.tooltip = {tooltip};
} }
EmotePtr cachedOrMake(Emote &&emote, const EmoteId &id) EmotePtr cachedOrMake(Emote &&emote, const EmoteId &id)
{ {
static std::unordered_map<EmoteId, std::weak_ptr<const Emote>> cache; static std::unordered_map<EmoteId, std::weak_ptr<const Emote>> cache;
@ -52,25 +55,57 @@ namespace {
return cachedOrMakeEmotePtr(std::move(emote), cache, mutex, id); return cachedOrMakeEmotePtr(std::move(emote), cache, mutex, id);
} }
std::pair<Outcome, EmoteMap> parseGlobalEmotes(
const QJsonObject &jsonRoot, const EmoteMap &currentEmotes) void parseEmoteSetInto(const QJsonObject &emoteSet, const QString &kind,
EmoteMap &map)
{
for (const auto emoteRef : emoteSet["emoticons"].toArray())
{
const auto emoteJson = emoteRef.toObject();
// margins
auto id = EmoteId{QString::number(emoteJson["id"].toInt())};
auto name = EmoteName{emoteJson["name"].toString()};
auto author =
EmoteAuthor{emoteJson["owner"]["display_name"].toString()};
auto urls = emoteJson["urls"].toObject();
if (emoteJson["animated"].isObject())
{
// prefer animated images if available
urls = emoteJson["animated"].toObject();
}
Emote emote;
fillInEmoteData(urls, name,
QString("%1<br>%2 FFZ Emote<br>By: %3")
.arg(name.string, kind, author.string),
emote);
emote.homePage =
Url{QString("https://www.frankerfacez.com/emoticon/%1-%2")
.arg(id.string)
.arg(name.string)};
map[name] = cachedOrMake(std::move(emote), id);
}
}
EmoteMap parseGlobalEmotes(const QJsonObject &jsonRoot)
{ {
// Load default sets from the `default_sets` object // Load default sets from the `default_sets` object
std::unordered_set<int> defaultSets{}; std::unordered_set<int> defaultSets{};
auto jsonDefaultSets = jsonRoot.value("default_sets").toArray(); auto jsonDefaultSets = jsonRoot["default_sets"].toArray();
for (auto jsonDefaultSet : jsonDefaultSets) for (auto jsonDefaultSet : jsonDefaultSets)
{ {
defaultSets.insert(jsonDefaultSet.toInt()); defaultSets.insert(jsonDefaultSet.toInt());
} }
auto jsonSets = jsonRoot.value("sets").toObject();
auto emotes = EmoteMap(); auto emotes = EmoteMap();
for (auto jsonSet : jsonSets) for (const auto emoteSetRef : jsonRoot["sets"].toObject())
{ {
auto jsonSetObject = jsonSet.toObject(); const auto emoteSet = emoteSetRef.toObject();
const auto emoteSetID = jsonSetObject.value("id").toInt(); auto emoteSetID = emoteSet["id"].toInt();
if (defaultSets.find(emoteSetID) == defaultSets.end()) if (!defaultSets.contains(emoteSetID))
{ {
qCDebug(chatterinoFfzemotes) qCDebug(chatterinoFfzemotes)
<< "Skipping global emote set" << emoteSetID << "Skipping global emote set" << emoteSetID
@ -78,35 +113,14 @@ namespace {
continue; continue;
} }
auto jsonEmotes = jsonSetObject.value("emoticons").toArray(); parseEmoteSetInto(emoteSet, "Global", emotes);
for (auto jsonEmoteValue : jsonEmotes)
{
auto jsonEmote = jsonEmoteValue.toObject();
auto name = EmoteName{jsonEmote.value("name").toString()};
auto id =
EmoteId{QString::number(jsonEmote.value("id").toInt())};
auto urls = jsonEmote.value("urls").toObject();
auto emote = Emote();
fillInEmoteData(urls, name,
name.string + "<br>Global FFZ Emote", emote);
emote.homePage =
Url{QString("https://www.frankerfacez.com/emoticon/%1-%2")
.arg(id.string)
.arg(name.string)};
emotes[name] =
cachedOrMakeEmotePtr(std::move(emote), currentEmotes);
}
} }
return {Success, std::move(emotes)}; return emotes;
} }
boost::optional<EmotePtr> parseAuthorityBadge(const QJsonObject &badgeUrls, boost::optional<EmotePtr> parseAuthorityBadge(const QJsonObject &badgeUrls,
const QString tooltip) const QString &tooltip)
{ {
boost::optional<EmotePtr> authorityBadge; boost::optional<EmotePtr> authorityBadge;
@ -138,40 +152,11 @@ namespace {
EmoteMap parseChannelEmotes(const QJsonObject &jsonRoot) EmoteMap parseChannelEmotes(const QJsonObject &jsonRoot)
{ {
auto jsonSets = jsonRoot.value("sets").toObject();
auto emotes = EmoteMap(); auto emotes = EmoteMap();
for (auto jsonSet : jsonSets) for (const auto emoteSetRef : jsonRoot["sets"].toObject())
{ {
auto jsonEmotes = jsonSet.toObject().value("emoticons").toArray(); parseEmoteSetInto(emoteSetRef.toObject(), "Channel", emotes);
for (auto _jsonEmote : jsonEmotes)
{
auto jsonEmote = _jsonEmote.toObject();
// margins
auto id =
EmoteId{QString::number(jsonEmote.value("id").toInt())};
auto name = EmoteName{jsonEmote.value("name").toString()};
auto author = EmoteAuthor{jsonEmote.value("owner")
.toObject()
.value("display_name")
.toString()};
auto urls = jsonEmote.value("urls").toObject();
Emote emote;
fillInEmoteData(urls, name,
QString("%1<br>Channel FFZ Emote<br>By: %2")
.arg(name.string)
.arg(author.string),
emote);
emote.homePage =
Url{QString("https://www.frankerfacez.com/emoticon/%1-%2")
.arg(id.string)
.arg(name.string)};
emotes[name] = cachedOrMake(std::move(emote), id);
}
} }
return emotes; return emotes;
@ -193,7 +178,9 @@ boost::optional<EmotePtr> FfzEmotes::emote(const EmoteName &name) const
auto emotes = this->global_.get(); auto emotes = this->global_.get();
auto it = emotes->find(name); auto it = emotes->find(name);
if (it != emotes->end()) if (it != emotes->end())
{
return it->second; return it->second;
}
return boost::none; return boost::none;
} }
@ -211,41 +198,38 @@ void FfzEmotes::loadEmotes()
.timeout(30000) .timeout(30000)
.onSuccess([this](auto result) -> Outcome { .onSuccess([this](auto result) -> Outcome {
auto emotes = this->emotes(); auto parsedSet = parseGlobalEmotes(result.parseJson());
auto pair = parseGlobalEmotes(result.parseJson(), *emotes); this->global_.set(std::make_shared<EmoteMap>(std::move(parsedSet)));
if (pair.first)
this->global_.set( return Success;
std::make_shared<EmoteMap>(std::move(pair.second)));
return pair.first;
}) })
.execute(); .execute();
} }
void FfzEmotes::loadChannel( void FfzEmotes::loadChannel(
std::weak_ptr<Channel> channel, const QString &channelId, std::weak_ptr<Channel> channel, const QString &channelID,
std::function<void(EmoteMap &&)> emoteCallback, std::function<void(EmoteMap &&)> emoteCallback,
std::function<void(boost::optional<EmotePtr>)> modBadgeCallback, std::function<void(boost::optional<EmotePtr>)> modBadgeCallback,
std::function<void(boost::optional<EmotePtr>)> vipBadgeCallback, std::function<void(boost::optional<EmotePtr>)> vipBadgeCallback,
bool manualRefresh) bool manualRefresh)
{ {
qCDebug(chatterinoFfzemotes) qCDebug(chatterinoFfzemotes)
<< "[FFZEmotes] Reload FFZ Channel Emotes for channel" << channelId; << "[FFZEmotes] Reload FFZ Channel Emotes for channel" << channelID;
NetworkRequest("https://api.frankerfacez.com/v1/room/id/" + channelId) NetworkRequest("https://api.frankerfacez.com/v1/room/id/" + channelID)
.timeout(20000) .timeout(20000)
.onSuccess([emoteCallback = std::move(emoteCallback), .onSuccess([emoteCallback = std::move(emoteCallback),
modBadgeCallback = std::move(modBadgeCallback), modBadgeCallback = std::move(modBadgeCallback),
vipBadgeCallback = std::move(vipBadgeCallback), channel, vipBadgeCallback = std::move(vipBadgeCallback), channel,
manualRefresh](auto result) -> Outcome { manualRefresh](const auto &result) -> Outcome {
auto json = result.parseJson(); const auto json = result.parseJson();
auto emoteMap = parseChannelEmotes(json); auto emoteMap = parseChannelEmotes(json);
auto modBadge = parseAuthorityBadge( auto modBadge = parseAuthorityBadge(
json.value("room").toObject().value("mod_urls").toObject(), json["room"]["mod_urls"].toObject(), "Moderator");
"Moderator");
auto vipBadge = parseAuthorityBadge( auto vipBadge = parseAuthorityBadge(
json.value("room").toObject().value("vip_badge").toObject(), json["room"]["vip_badge"].toObject(), "VIP");
"VIP");
bool hasEmotes = !emoteMap.empty(); bool hasEmotes = !emoteMap.empty();
@ -268,22 +252,27 @@ void FfzEmotes::loadChannel(
return Success; return Success;
}) })
.onError([channelId, channel, manualRefresh](NetworkResult result) { .onError([channelID, channel, manualRefresh](const auto &result) {
auto shared = channel.lock(); auto shared = channel.lock();
if (!shared) if (!shared)
{
return; return;
}
if (result.status() == 404) if (result.status() == 404)
{ {
// User does not have any FFZ emotes // User does not have any FFZ emotes
if (manualRefresh) if (manualRefresh)
{
shared->addMessage( shared->addMessage(
makeSystemMessage(CHANNEL_HAS_NO_EMOTES)); makeSystemMessage(CHANNEL_HAS_NO_EMOTES));
} }
}
else if (result.status() == NetworkResult::timedoutStatus) else if (result.status() == NetworkResult::timedoutStatus)
{ {
// TODO: Auto retry in case of a timeout, with a delay // TODO: Auto retry in case of a timeout, with a delay
qCWarning(chatterinoFfzemotes) qCWarning(chatterinoFfzemotes)
<< "Fetching FFZ emotes for channel" << channelId << "Fetching FFZ emotes for channel" << channelID
<< "failed due to timeout"; << "failed due to timeout";
shared->addMessage( shared->addMessage(
makeSystemMessage("Failed to fetch FrankerFaceZ channel " makeSystemMessage("Failed to fetch FrankerFaceZ channel "
@ -292,7 +281,7 @@ void FfzEmotes::loadChannel(
else else
{ {
qCWarning(chatterinoFfzemotes) qCWarning(chatterinoFfzemotes)
<< "Error fetching FFZ emotes for channel" << channelId << "Error fetching FFZ emotes for channel" << channelID
<< ", error" << result.status(); << ", error" << result.status();
shared->addMessage( shared->addMessage(
makeSystemMessage("Failed to fetch FrankerFaceZ channel " makeSystemMessage("Failed to fetch FrankerFaceZ channel "

View file

@ -0,0 +1,12 @@
#include "providers/ffz/FfzUtil.hpp"
namespace chatterino {
Url parseFfzUrl(const QString &ffzUrl)
{
QUrl asURL(ffzUrl);
asURL.setScheme("https");
return {asURL.toString()};
}
} // namespace chatterino

View file

@ -0,0 +1,12 @@
#pragma once
#include "common/Aliases.hpp"
#include <QString>
#include <QUrl>
namespace chatterino {
Url parseFfzUrl(const QString &ffzUrl);
} // namespace chatterino

View file

@ -45,7 +45,7 @@ void SeventvBadges::loadSeventvBadges()
.onSuccess([this](const NetworkResult &result) -> Outcome { .onSuccess([this](const NetworkResult &result) -> Outcome {
auto root = result.parseJson(); auto root = result.parseJson();
std::shared_lock lock(this->mutex_); std::unique_lock lock(this->mutex_);
int index = 0; int index = 0;
for (const auto &jsonBadge : root.value("badges").toArray()) for (const auto &jsonBadge : root.value("badges").toArray())

View file

@ -9,10 +9,11 @@
#include "util/DisplayBadge.hpp" #include "util/DisplayBadge.hpp"
#include <QBuffer> #include <QBuffer>
#include <QFile>
#include <QIcon> #include <QIcon>
#include <QImageReader> #include <QImageReader>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonObject> #include <QJsonDocument>
#include <QJsonValue> #include <QJsonValue>
#include <QThread> #include <QThread>
#include <QUrlQuery> #include <QUrlQuery>
@ -36,57 +37,70 @@ void TwitchBadges::loadTwitchBadges()
NetworkRequest(url) NetworkRequest(url)
.onSuccess([this](auto result) -> Outcome { .onSuccess([this](auto result) -> Outcome {
{
auto root = result.parseJson(); auto root = result.parseJson();
this->parseTwitchBadges(root);
this->loaded();
return Success;
})
.onError([this](auto res) {
qCWarning(chatterinoTwitch)
<< "Error loading Twitch Badges from the badges API:"
<< res.status() << " - falling back to backup";
QFile file(":/twitch-badges.json");
if (!file.open(QFile::ReadOnly))
{
// Despite erroring out, we still want to reach the same point
// Loaded should still be set to true to not build up an endless queue, and the quuee should still be flushed.
qCWarning(chatterinoTwitch)
<< "Error loading Twitch Badges from the local backup file";
this->loaded();
return;
}
auto bytes = file.readAll();
auto doc = QJsonDocument::fromJson(bytes);
this->parseTwitchBadges(doc.object());
this->loaded();
})
.execute();
}
void TwitchBadges::parseTwitchBadges(QJsonObject root)
{
auto badgeSets = this->badgeSets_.access(); auto badgeSets = this->badgeSets_.access();
auto jsonSets = root.value("badge_sets").toObject(); auto jsonSets = root.value("badge_sets").toObject();
for (auto sIt = jsonSets.begin(); sIt != jsonSets.end(); ++sIt) for (auto sIt = jsonSets.begin(); sIt != jsonSets.end(); ++sIt)
{ {
auto key = sIt.key(); auto key = sIt.key();
auto versions = auto versions = sIt.value().toObject().value("versions").toObject();
sIt.value().toObject().value("versions").toObject();
for (auto vIt = versions.begin(); vIt != versions.end(); for (auto vIt = versions.begin(); vIt != versions.end(); ++vIt)
++vIt)
{ {
auto versionObj = vIt.value().toObject(); auto versionObj = vIt.value().toObject();
auto emote = Emote{ auto emote = Emote{
{""}, {""},
ImageSet{ ImageSet{
Image::fromUrl({versionObj.value("image_url_1x") Image::fromUrl(
.toString()}, {versionObj.value("image_url_1x").toString()}, 1),
1), Image::fromUrl(
Image::fromUrl({versionObj.value("image_url_2x") {versionObj.value("image_url_2x").toString()}, .5),
.toString()}, Image::fromUrl(
.5), {versionObj.value("image_url_4x").toString()}, .25),
Image::fromUrl({versionObj.value("image_url_4x")
.toString()},
.25),
}, },
Tooltip{versionObj.value("title").toString()}, Tooltip{versionObj.value("title").toString()},
Url{versionObj.value("click_url").toString()}}; Url{versionObj.value("click_url").toString()}};
// "title" // "title"
// "clickAction" // "clickAction"
(*badgeSets)[key][vIt.key()] = (*badgeSets)[key][vIt.key()] = std::make_shared<Emote>(emote);
std::make_shared<Emote>(emote);
} }
} }
} }
this->loaded();
return Success;
})
.onError([this](auto res) {
qCDebug(chatterinoTwitch)
<< "Error loading Twitch Badges:" << res.status();
// Despite erroring out, we still want to reach the same point
// Loaded should still be set to true to not build up an endless queue, and the quuee should still be flushed.
this->loaded();
})
.execute();
}
void TwitchBadges::loaded() void TwitchBadges::loaded()
{ {

View file

@ -6,6 +6,7 @@
#include <boost/optional.hpp> #include <boost/optional.hpp>
#include <pajlada/signals/signal.hpp> #include <pajlada/signals/signal.hpp>
#include <QIcon> #include <QIcon>
#include <QJsonObject>
#include <QMap> #include <QMap>
#include <QString> #include <QString>
@ -49,6 +50,10 @@ private:
TwitchBadges(); TwitchBadges();
void loadTwitchBadges(); void loadTwitchBadges();
/**
* @brief Accepts a JSON blob from https://badges.twitch.tv/v1/badges/global/display and updates our badges with it
**/
void parseTwitchBadges(QJsonObject root);
void loaded(); void loaded();
void loadEmoteImage(const QString &name, ImagePtr image, void loadEmoteImage(const QString &name, ImagePtr image,
BadgeIconCallback &&callback); BadgeIconCallback &&callback);

View file

@ -1023,6 +1023,7 @@ Outcome TwitchMessageBuilder::tryAppendEmote(const EmoteName &name)
auto flags = MessageElementFlags(); auto flags = MessageElementFlags();
auto emote = boost::optional<EmotePtr>{}; auto emote = boost::optional<EmotePtr>{};
bool zeroWidth = false;
// Emote order: // Emote order:
// - FrankerFaceZ Channel // - FrankerFaceZ Channel
@ -1044,10 +1045,7 @@ Outcome TwitchMessageBuilder::tryAppendEmote(const EmoteName &name)
(emote = this->twitchChannel->seventvEmote(name))) (emote = this->twitchChannel->seventvEmote(name)))
{ {
flags = MessageElementFlag::SevenTVEmote; flags = MessageElementFlag::SevenTVEmote;
if (emote.value()->zeroWidth) zeroWidth = emote.value()->zeroWidth;
{
flags.set(MessageElementFlag::ZeroWidthEmote);
}
} }
else if ((emote = globalFfzEmotes.emote(name))) else if ((emote = globalFfzEmotes.emote(name)))
{ {
@ -1056,23 +1054,48 @@ Outcome TwitchMessageBuilder::tryAppendEmote(const EmoteName &name)
else if ((emote = globalBttvEmotes.emote(name))) else if ((emote = globalBttvEmotes.emote(name)))
{ {
flags = MessageElementFlag::BttvEmote; flags = MessageElementFlag::BttvEmote;
zeroWidth = zeroWidthEmotes.contains(name.string);
if (zeroWidthEmotes.contains(name.string))
{
flags.set(MessageElementFlag::ZeroWidthEmote);
}
} }
else if ((emote = globalSeventvEmotes.globalEmote(name))) else if ((emote = globalSeventvEmotes.globalEmote(name)))
{ {
flags = MessageElementFlag::SevenTVEmote; flags = MessageElementFlag::SevenTVEmote;
if (emote.value()->zeroWidth) zeroWidth = emote.value()->zeroWidth;
{
flags.set(MessageElementFlag::ZeroWidthEmote);
}
} }
if (emote) if (emote)
{ {
if (zeroWidth && getSettings()->enableZeroWidthEmotes &&
!this->isEmpty())
{
// Attempt to merge current zero-width emote into any previous emotes
auto asEmote = dynamic_cast<EmoteElement *>(&this->back());
if (asEmote)
{
// Make sure to access asEmote before taking ownership when releasing
auto baseEmote = asEmote->getEmote();
// Need to remove EmoteElement and replace with LayeredEmoteElement
auto baseEmoteElement = this->releaseBack();
std::vector<LayeredEmoteElement::Emote> layers = {
{baseEmote, baseEmoteElement->getFlags()},
{emote.get(), flags}};
this->emplace<LayeredEmoteElement>(
std::move(layers), baseEmoteElement->getFlags() | flags,
this->textColor_);
return Success;
}
auto asLayered = dynamic_cast<LayeredEmoteElement *>(&this->back());
if (asLayered)
{
asLayered->addEmoteLayer({emote.get(), flags});
asLayered->addFlags(flags);
return Success;
}
// No emote to merge with, just show as regular emote
}
this->emplace<EmoteElement>(emote.get(), flags, this->textColor_); this->emplace<EmoteElement>(emote.get(), flags, this->textColor_);
return Success; return Success;
} }

View file

@ -213,6 +213,7 @@ public:
false}; false};
BoolSetting enableEmoteImages = {"/emotes/enableEmoteImages", true}; BoolSetting enableEmoteImages = {"/emotes/enableEmoteImages", true};
BoolSetting animateEmotes = {"/emotes/enableGifAnimations", true}; BoolSetting animateEmotes = {"/emotes/enableGifAnimations", true};
BoolSetting enableZeroWidthEmotes = {"/emotes/enableZeroWidthEmotes", true};
FloatSetting emoteScale = {"/emotes/scale", 1.f}; FloatSetting emoteScale = {"/emotes/scale", 1.f};
BoolSetting showUnlistedSevenTVEmotes = { BoolSetting showUnlistedSevenTVEmotes = {
"/emotes/showUnlistedSevenTVEmotes", false}; "/emotes/showUnlistedSevenTVEmotes", false};

View file

@ -8,8 +8,6 @@
#include <cmath> #include <cmath>
#define LOOKUP_COLOR_COUNT 360
namespace { namespace {
double getMultiplierByTheme(const QString &themeName) double getMultiplierByTheme(const QString &themeName)
{ {
@ -17,26 +15,20 @@ double getMultiplierByTheme(const QString &themeName)
{ {
return 0.8; return 0.8;
} }
else if (themeName == "White") if (themeName == "White")
{ {
return 1.0; return 1.0;
} }
else if (themeName == "Black") if (themeName == "Black")
{ {
return -1.0; return -1.0;
} }
else if (themeName == "Dark") if (themeName == "Dark")
{ {
return -0.8; return -0.8;
} }
/*
else if (themeName == "Custom")
{
return getSettings()->customThemeMultiplier.getValue();
}
*/
return -0.8; return -0.8; // default: Dark
} }
} // namespace } // namespace
@ -47,16 +39,6 @@ bool Theme::isLightTheme() const
return this->isLight_; return this->isLight_;
} }
QColor Theme::blendColors(const QColor &color1, const QColor &color2,
qreal ratio)
{
int r = int(color1.red() * (1 - ratio) + color2.red() * ratio);
int g = int(color1.green() * (1 - ratio) + color2.green() * ratio);
int b = int(color1.blue() * (1 - ratio) + color2.blue() * ratio);
return QColor(r, g, b, 255);
}
Theme::Theme() Theme::Theme()
{ {
this->update(); this->update();
@ -66,149 +48,84 @@ Theme::Theme()
this->update(); this->update();
}, },
false); false);
this->themeHue.connectSimple(
[this](auto) {
this->update();
},
false);
} }
void Theme::update() void Theme::update()
{ {
this->actuallyUpdate(this->themeHue, this->actuallyUpdate(getMultiplierByTheme(this->themeName.getValue()));
getMultiplierByTheme(this->themeName.getValue()));
this->updated.invoke(); this->updated.invoke();
} }
// hue: theme color (0 - 1)
// multiplier: 1 = white, 0.8 = light, -0.8 dark, -1 black // multiplier: 1 = white, 0.8 = light, -0.8 dark, -1 black
void Theme::actuallyUpdate(double hue, double multiplier) void Theme::actuallyUpdate(double multiplier)
{ {
this->isLight_ = multiplier > 0; this->isLight_ = multiplier > 0;
bool lightWin = isLight_;
// QColor themeColor = QColor::fromHslF(hue, 0.43, 0.5);
QColor themeColor = QColor::fromHslF(hue, 0.8, 0.5);
QColor themeColorNoSat = QColor::fromHslF(hue, 0, 0.5);
const auto sat = qreal(0);
const auto isLight = this->isLightTheme(); const auto isLight = this->isLightTheme();
const auto flat = isLight;
auto getColor = [multiplier](double h, double s, double l, double a = 1.0) { auto getGray = [multiplier](double l, double a = 1.0) {
return QColor::fromHslF(h, s, ((l - 0.5) * multiplier) + 0.5, a); return QColor::fromHslF(0, 0, ((l - 0.5) * multiplier) + 0.5, a);
}; };
/// WINDOW /// WINDOW
{
#ifdef Q_OS_LINUX #ifdef Q_OS_LINUX
this->window.background = lightWin ? "#fff" : QColor(61, 60, 56); this->window.background = isLight ? "#fff" : QColor(61, 60, 56);
#else #else
this->window.background = lightWin ? "#fff" : "#111"; this->window.background = isLight ? "#fff" : "#111";
#endif #endif
this->window.text = isLight ? "#000" : "#eee";
QColor fg = this->window.text = lightWin ? "#000" : "#eee"; /// TABSs
this->window.borderFocused = lightWin ? "#ccc" : themeColor; if (isLight)
this->window.borderUnfocused = lightWin ? "#ccc" : themeColorNoSat;
// Ubuntu style
// TODO: add setting for this
// TabText = QColor(210, 210, 210);
// TabBackground = QColor(61, 60, 56);
// TabHoverText = QColor(210, 210, 210);
// TabHoverBackground = QColor(73, 72, 68);
// message (referenced later)
this->messages.textColors.caret = //
this->messages.textColors.regular = isLight_ ? "#000" : "#fff";
QColor highlighted = lightWin ? QColor("#ff0000") : QColor("#ee6166");
/// TABS
if (lightWin)
{ {
this->tabs.regular = { this->tabs.regular = {.text = "#444",
QColor("#444"), .backgrounds = {"#fff", "#eee", "#fff"},
{QColor("#fff"), QColor("#eee"), QColor("#fff")}, .line = {"#fff", "#fff", "#fff"}};
{QColor("#fff"), QColor("#fff"), QColor("#fff")}}; this->tabs.newMessage = {.text = "#222",
this->tabs.newMessage = { .backgrounds = {"#fff", "#eee", "#fff"},
QColor("#222"), .line = {"#bbb", "#bbb", "#bbb"}};
{QColor("#fff"), QColor("#eee"), QColor("#fff")}, this->tabs.highlighted = {.text = "#000",
{QColor("#bbb"), QColor("#bbb"), QColor("#bbb")}}; .backgrounds = {"#fff", "#eee", "#fff"},
this->tabs.highlighted = { .line = {"#f00", "#f00", "#f00"}};
fg,
{QColor("#fff"), QColor("#eee"), QColor("#fff")},
{highlighted, highlighted, highlighted}};
this->tabs.selected = { this->tabs.selected = {
QColor("#000"), .text = "#000",
{QColor("#b4d7ff"), QColor("#b4d7ff"), QColor("#b4d7ff")}, .backgrounds = {"#b4d7ff", "#b4d7ff", "#b4d7ff"},
{this->accent, this->accent, this->accent}}; .line = {this->accent, this->accent, this->accent}};
} }
else else
{ {
this->tabs.regular = { this->tabs.regular = {.text = "#aaa",
QColor("#aaa"), .backgrounds{"#252525", "#252525", "#252525"},
{QColor("#252525"), QColor("#252525"), QColor("#252525")}, .line = {"#444", "#444", "#444"}};
{QColor("#444"), QColor("#444"), QColor("#444")}}; this->tabs.newMessage = {.text = "#eee",
this->tabs.newMessage = { .backgrounds{"#252525", "#252525", "#252525"},
fg, .line = {"#888", "#888", "#888"}};
{QColor("#252525"), QColor("#252525"), QColor("#252525")}, this->tabs.highlighted = {.text = "#eee",
{QColor("#888"), QColor("#888"), QColor("#888")}}; .backgrounds{"#252525", "#252525", "#252525"},
this->tabs.highlighted = { .line = {"#ee6166", "#ee6166", "#ee6166"}};
fg,
{QColor("#252525"), QColor("#252525"), QColor("#252525")},
{highlighted, highlighted, highlighted}};
this->tabs.selected = { this->tabs.selected = {
QColor("#fff"), .text = "#fff",
{QColor("#555555"), QColor("#555555"), QColor("#555555")}, .backgrounds{"#555", "#555", "#555"},
{this->accent, this->accent, this->accent}}; .line = {this->accent, this->accent, this->accent}};
} }
// scrollbar this->tabs.dividerLine = this->tabs.selected.backgrounds.regular;
this->scrollbars.highlights.highlight = QColor("#ee6166");
this->scrollbars.highlights.subscription = QColor("#C466FF");
// this->tabs.newMessage = {
// fg,
// {QBrush(blendColors(themeColor, "#ccc", 0.9), Qt::FDiagPattern),
// QBrush(blendColors(themeColor, "#ccc", 0.9), Qt::FDiagPattern),
// QBrush(blendColors(themeColorNoSat, "#ccc", 0.9),
// Qt::FDiagPattern)}};
// this->tabs.newMessage = {
// fg,
// {QBrush(blendColors(themeColor, "#666", 0.7),
// Qt::FDiagPattern),
// QBrush(blendColors(themeColor, "#666", 0.5),
// Qt::FDiagPattern),
// QBrush(blendColors(themeColorNoSat, "#666", 0.7),
// Qt::FDiagPattern)}};
// this->tabs.highlighted = {fg, {QColor("#777"),
// QColor("#777"), QColor("#666")}};
this->tabs.dividerLine =
this->tabs.selected.backgrounds.regular.color();
}
// Message // Message
this->messages.textColors.link = this->messages.textColors.caret = isLight ? "#000" : "#fff";
isLight_ ? QColor(66, 134, 244) : QColor(66, 134, 244); this->messages.textColors.regular = isLight ? "#000" : "#fff";
this->messages.textColors.link = QColor(66, 134, 244);
this->messages.textColors.system = QColor(140, 127, 127); this->messages.textColors.system = QColor(140, 127, 127);
this->messages.textColors.chatPlaceholder = this->messages.textColors.chatPlaceholder =
isLight_ ? QColor(175, 159, 159) : QColor(93, 85, 85); isLight ? QColor(175, 159, 159) : QColor(93, 85, 85);
this->messages.backgrounds.regular = getColor(0, sat, 1); this->messages.backgrounds.regular = getGray(1);
this->messages.backgrounds.alternate = getColor(0, sat, 0.96); this->messages.backgrounds.alternate = getGray(0.96);
// this->messages.backgrounds.resub this->messages.disabled = getGray(1, 0.6);
// this->messages.backgrounds.whisper
this->messages.disabled = getColor(0, sat, 1, 0.6);
// this->messages.seperator =
// this->messages.seperatorInner =
int complementaryGray = this->isLightTheme() ? 20 : 230; int complementaryGray = isLight ? 20 : 230;
this->messages.highlightAnimationStart = this->messages.highlightAnimationStart =
QColor(complementaryGray, complementaryGray, complementaryGray, 110); QColor(complementaryGray, complementaryGray, complementaryGray, 110);
this->messages.highlightAnimationEnd = this->messages.highlightAnimationEnd =
@ -216,66 +133,52 @@ void Theme::actuallyUpdate(double hue, double multiplier)
// Scrollbar // Scrollbar
this->scrollbars.background = QColor(0, 0, 0, 0); this->scrollbars.background = QColor(0, 0, 0, 0);
// this->scrollbars.background = splits.background; this->scrollbars.thumb = getGray(0.70);
// this->scrollbars.background.setAlphaF(qreal(0.2)); this->scrollbars.thumbSelected = getGray(0.65);
this->scrollbars.thumb = getColor(0, sat, 0.70);
this->scrollbars.thumbSelected = getColor(0, sat, 0.65);
// tooltip
this->tooltip.background = QColor(0, 0, 0);
this->tooltip.text = QColor(255, 255, 255);
// Selection // Selection
this->messages.selection = this->messages.selection =
isLightTheme() ? QColor(0, 0, 0, 64) : QColor(255, 255, 255, 64); isLight ? QColor(0, 0, 0, 64) : QColor(255, 255, 255, 64);
if (this->isLightTheme()) // Splits
if (isLight)
{ {
this->splits.dropTargetRect = QColor(255, 255, 255, 0x00); this->splits.dropTargetRect = QColor(255, 255, 255, 0);
this->splits.dropTargetRectBorder = QColor(0, 148, 255, 0x00);
this->splits.resizeHandle = QColor(0, 148, 255, 0xff);
this->splits.resizeHandleBackground = QColor(0, 148, 255, 0x50);
} }
else else
{ {
this->splits.dropTargetRect = QColor(0, 148, 255, 0x00); this->splits.dropTargetRect = QColor(0, 148, 255, 0);
this->splits.dropTargetRectBorder = QColor(0, 148, 255, 0x00);
this->splits.resizeHandle = QColor(0, 148, 255, 0x70);
this->splits.resizeHandleBackground = QColor(0, 148, 255, 0x20);
} }
this->splits.dropTargetRectBorder = QColor(0, 148, 255, 0);
this->splits.dropPreview = QColor(0, 148, 255, 48);
this->splits.dropPreviewBorder = QColor(0, 148, 255);
this->splits.resizeHandle = QColor(0, 148, 255, isLight ? 255 : 112);
this->splits.resizeHandleBackground =
QColor(0, 148, 255, isLight ? 80 : 32);
this->splits.header.background = getColor(0, sat, flat ? 1 : 0.9); this->splits.header.background = getGray(isLight ? 1 : 0.9);
this->splits.header.border = getColor(0, sat, flat ? 1 : 0.85); this->splits.header.border = getGray(isLight ? 1 : 0.85);
this->splits.header.text = this->messages.textColors.regular; this->splits.header.text = this->messages.textColors.regular;
this->splits.header.focusedBackground = this->splits.header.focusedBackground = getGray(isLight ? 0.95 : 0.79);
getColor(0, sat, isLight ? 0.95 : 0.79); this->splits.header.focusedBorder = getGray(isLight ? 0.90 : 0.78);
this->splits.header.focusedBorder = getColor(0, sat, isLight ? 0.90 : 0.78);
this->splits.header.focusedText = QColor::fromHsvF( this->splits.header.focusedText = QColor::fromHsvF(
0.58388, isLight ? 1.0 : 0.482, isLight ? 0.6375 : 1.0); 0.58388, isLight ? 1.0 : 0.482, isLight ? 0.6375 : 1.0);
this->splits.input.background = getColor(0, sat, flat ? 0.95 : 0.95); this->splits.input.background = getGray(0.95);
this->splits.input.border = getColor(0, sat, flat ? 1 : 1);
this->splits.input.text = this->messages.textColors.regular; this->splits.input.text = this->messages.textColors.regular;
this->splits.input.styleSheet = this->splits.input.styleSheet =
"background:" + this->splits.input.background.name() + ";" + "background:" + this->splits.input.background.name() + ";" +
"border:" + this->tabs.selected.backgrounds.regular.color().name() + "border:" + this->tabs.selected.backgrounds.regular.name() + ";" +
";" + "color:" + this->messages.textColors.regular.name() + ";" + "color:" + this->messages.textColors.regular.name() + ";" +
"selection-background-color:" + "selection-background-color:" +
(isLight ? "#68B1FF" (isLight ? "#68B1FF" : this->tabs.selected.backgrounds.regular.name());
: this->tabs.selected.backgrounds.regular.color().name());
this->splits.input.focusedLine = this->tabs.highlighted.line.regular;
this->splits.messageSeperator = this->splits.messageSeperator =
isLight ? QColor(127, 127, 127) : QColor(60, 60, 60); isLight ? QColor(127, 127, 127) : QColor(60, 60, 60);
this->splits.background = getColor(0, sat, 1); this->splits.background = getGray(1);
this->splits.dropPreview = QColor(0, 148, 255, 0x30);
this->splits.dropPreviewBorder = QColor(0, 148, 255, 0xff);
// Copy button // Copy button
if (this->isLightTheme()) if (isLight)
{ {
this->buttons.copy = getResources().buttons.copyDark; this->buttons.copy = getResources().buttons.copyDark;
this->buttons.pin = getResources().buttons.pinDisabledDark; this->buttons.pin = getResources().buttons.pinDisabledDark;
@ -287,7 +190,7 @@ void Theme::actuallyUpdate(double hue, double multiplier)
} }
} }
void Theme::normalizeColor(QColor &color) void Theme::normalizeColor(QColor &color) const
{ {
if (this->isLightTheme()) if (this->isLightTheme())
{ {

View file

@ -5,8 +5,8 @@
#include "util/RapidJsonSerializeQString.hpp" #include "util/RapidJsonSerializeQString.hpp"
#include <pajlada/settings/setting.hpp> #include <pajlada/settings/setting.hpp>
#include <QBrush>
#include <QColor> #include <QColor>
#include <QPixmap>
namespace chatterino { namespace chatterino {
@ -22,9 +22,9 @@ public:
struct TabColors { struct TabColors {
QColor text; QColor text;
struct { struct {
QBrush regular; QColor regular;
QBrush hover; QColor hover;
QBrush unfocused; QColor unfocused;
} backgrounds; } backgrounds;
struct { struct {
QColor regular; QColor regular;
@ -39,8 +39,6 @@ public:
struct { struct {
QColor background; QColor background;
QColor text; QColor text;
QColor borderUnfocused;
QColor borderFocused;
} window; } window;
/// TABS /// TABS
@ -49,7 +47,6 @@ public:
TabColors newMessage; TabColors newMessage;
TabColors highlighted; TabColors highlighted;
TabColors selected; TabColors selected;
QColor border;
QColor dividerLine; QColor dividerLine;
} tabs; } tabs;
@ -66,12 +63,9 @@ public:
struct { struct {
QColor regular; QColor regular;
QColor alternate; QColor alternate;
// QColor whisper;
} backgrounds; } backgrounds;
QColor disabled; QColor disabled;
// QColor seperator;
// QColor seperatorInner;
QColor selection; QColor selection;
QColor highlightAnimationStart; QColor highlightAnimationStart;
@ -83,18 +77,8 @@ public:
QColor background; QColor background;
QColor thumb; QColor thumb;
QColor thumbSelected; QColor thumbSelected;
struct {
QColor highlight;
QColor subscription;
} highlights;
} scrollbars; } scrollbars;
/// TOOLTIP
struct {
QColor text;
QColor background;
} tooltip;
/// SPLITS /// SPLITS
struct { struct {
QColor messageSeperator; QColor messageSeperator;
@ -113,17 +97,12 @@ public:
QColor focusedBackground; QColor focusedBackground;
QColor text; QColor text;
QColor focusedText; QColor focusedText;
// int margin;
} header; } header;
struct { struct {
QColor border;
QColor background; QColor background;
QColor selection;
QColor focusedLine;
QColor text; QColor text;
QString styleSheet; QString styleSheet;
// int margin;
} input; } input;
} splits; } splits;
@ -132,18 +111,16 @@ public:
QPixmap pin; QPixmap pin;
} buttons; } buttons;
void normalizeColor(QColor &color); void normalizeColor(QColor &color) const;
void update(); void update();
QColor blendColors(const QColor &color1, const QColor &color2, qreal ratio);
pajlada::Signals::NoArgSignal updated; pajlada::Signals::NoArgSignal updated;
QStringSetting themeName{"/appearance/theme/name", "Dark"}; QStringSetting themeName{"/appearance/theme/name", "Dark"};
DoubleSetting themeHue{"/appearance/theme/hue", 0.0};
private: private:
bool isLight_ = false; bool isLight_ = false;
void actuallyUpdate(double hue, double multiplier); void actuallyUpdate(double multiplier);
pajlada::Signals::NoArgSignal repaintVisibleChatWidgets_; pajlada::Signals::NoArgSignal repaintVisibleChatWidgets_;

View file

@ -2,6 +2,7 @@
#include "common/QLogging.hpp" #include "common/QLogging.hpp"
#include "messages/Message.hpp" #include "messages/Message.hpp"
#include "messages/MessageThread.hpp"
#include "singletons/Paths.hpp" #include "singletons/Paths.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
@ -104,7 +105,20 @@ void LoggingChannel::addMessage(MessagePtr message)
str.append(now.toString("HH:mm:ss")); str.append(now.toString("HH:mm:ss"));
str.append("] "); str.append("] ");
str.append(message->searchText); QString messageSearchText = message->searchText;
if ((message->flags.has(MessageFlag::ReplyMessage) &&
getSettings()->stripReplyMention) &&
!getSettings()->hideReplyContext)
{
qsizetype colonIndex = messageSearchText.indexOf(':');
if (colonIndex != -1)
{
QString rootMessageChatter =
message->replyThread->root()->loginName;
messageSearchText.insert(colonIndex + 1, " @" + rootMessageChatter);
}
}
str.append(messageSearchText);
str.append(endline); str.append(endline);
this->appendLine(str); this->appendLine(str);

View file

@ -162,24 +162,6 @@ QString shortenString(const QString &str, unsigned maxWidth)
return shortened; return shortened;
} }
QString localizeNumbers(const int &number)
{
QLocale locale;
return locale.toString(number);
}
QString localizeNumbers(unsigned int number)
{
QLocale locale;
return locale.toString(number);
}
QString localizeNumbers(qsizetype number)
{
QLocale locale;
return locale.toString(number);
}
QString kFormatNumbers(const int &number) QString kFormatNumbers(const int &number)
{ {
return QString("%1K").arg(number / 1000); return QString("%1K").arg(number / 1000);

View file

@ -1,6 +1,7 @@
#pragma once #pragma once
#include <QColor> #include <QColor>
#include <QLocale>
#include <QString> #include <QString>
#include <QStringRef> #include <QStringRef>
@ -72,9 +73,12 @@ QString formatRichNamedLink(const QString &url, const QString &name,
QString shortenString(const QString &str, unsigned maxWidth = 50); QString shortenString(const QString &str, unsigned maxWidth = 50);
QString localizeNumbers(const int &number); template <typename T>
QString localizeNumbers(unsigned int number); QString localizeNumbers(T number)
QString localizeNumbers(qsizetype number); {
QLocale locale;
return locale.toString(number);
}
QString kFormatNumbers(const int &number); QString kFormatNumbers(const int &number);

View file

@ -3,6 +3,7 @@
#ifdef USEWINSDK #ifdef USEWINSDK
# include <boost/optional.hpp> # include <boost/optional.hpp>
# include <QString>
# include <Windows.h> # include <Windows.h>
namespace chatterino { namespace chatterino {

View file

@ -0,0 +1,119 @@
#include "widgets/TooltipEntryWidget.hpp"
#include <QVBoxLayout>
namespace chatterino {
TooltipEntryWidget::TooltipEntryWidget(QWidget *parent)
: TooltipEntryWidget(nullptr, "", 0, 0, parent)
{
}
TooltipEntryWidget::TooltipEntryWidget(ImagePtr image, const QString &text,
int customWidth, int customHeight,
QWidget *parent)
: QWidget(parent)
, image_(image)
, customImgWidth_(customWidth)
, customImgHeight_(customHeight)
{
auto *layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
this->setLayout(layout);
this->displayImage_ = new QLabel();
this->displayImage_->setAlignment(Qt::AlignHCenter);
this->displayImage_->setStyleSheet("background: transparent");
this->displayText_ = new QLabel(text);
this->displayText_->setAlignment(Qt::AlignHCenter);
this->displayText_->setStyleSheet("background: transparent");
layout->addWidget(this->displayImage_);
layout->addWidget(this->displayText_);
}
void TooltipEntryWidget::setWordWrap(bool wrap)
{
this->displayText_->setWordWrap(wrap);
}
void TooltipEntryWidget::setImageScale(int w, int h)
{
if (this->customImgWidth_ == w && this->customImgHeight_ == h)
{
return;
}
this->customImgWidth_ = w;
this->customImgHeight_ = h;
this->refreshPixmap();
}
void TooltipEntryWidget::setText(const QString &text)
{
this->displayText_->setText(text);
}
void TooltipEntryWidget::setImage(ImagePtr image)
{
if (this->image_ == image)
{
return;
}
this->clearImage();
this->image_ = std::move(image);
this->refreshPixmap();
}
void TooltipEntryWidget::clearImage()
{
this->displayImage_->hide();
this->image_ = nullptr;
this->setImageScale(0, 0);
}
bool TooltipEntryWidget::refreshPixmap()
{
if (!this->image_)
{
return false;
}
auto pixmap = this->image_->pixmapOrLoad();
if (!pixmap)
{
this->attemptRefresh_ = true;
return false;
}
if (this->customImgWidth_ > 0 || this->customImgHeight_ > 0)
{
this->displayImage_->setPixmap(pixmap->scaled(this->customImgWidth_,
this->customImgHeight_,
Qt::KeepAspectRatio));
}
else
{
this->displayImage_->setPixmap(*pixmap);
}
this->displayImage_->show();
return true;
}
bool TooltipEntryWidget::animated() const
{
return this->image_ && this->image_->animated();
}
bool TooltipEntryWidget::hasImage() const
{
return this->image_ != nullptr;
}
bool TooltipEntryWidget::attemptRefresh() const
{
return this->attemptRefresh_;
}
} // namespace chatterino

View file

@ -0,0 +1,42 @@
#pragma once
#include "messages/Image.hpp"
#include <QLabel>
#include <QWidget>
namespace chatterino {
class TooltipEntryWidget : public QWidget
{
Q_OBJECT
public:
TooltipEntryWidget(QWidget *parent = nullptr);
TooltipEntryWidget(ImagePtr image, const QString &text, int customWidth,
int customHeight, QWidget *parent = nullptr);
void setImageScale(int w, int h);
void setWordWrap(bool wrap);
void setText(const QString &text);
void setImage(ImagePtr image);
void clearImage();
bool refreshPixmap();
bool animated() const;
bool hasImage() const;
bool attemptRefresh() const;
private:
QLabel *displayImage_ = nullptr;
QLabel *displayText_ = nullptr;
bool attemptRefresh_ = false;
ImagePtr image_ = nullptr;
int customImgWidth_ = 0;
int customImgHeight_ = 0;
};
} // namespace chatterino

View file

@ -6,7 +6,9 @@
#include "singletons/WindowManager.hpp" #include "singletons/WindowManager.hpp"
#include <QPainter> #include <QPainter>
#include <QVBoxLayout>
// number of columns in grid mode
#define GRID_NUM_COLS 3
namespace chatterino { namespace chatterino {
@ -20,8 +22,6 @@ TooltipWidget::TooltipWidget(BaseWidget *parent)
: BaseWindow({BaseWindow::TopMost, BaseWindow::DontFocus, : BaseWindow({BaseWindow::TopMost, BaseWindow::DontFocus,
BaseWindow::DisableLayoutSave}, BaseWindow::DisableLayoutSave},
parent) parent)
, displayImage_(new QLabel(this))
, displayText_(new QLabel(this))
{ {
this->setStyleSheet("color: #fff; background: rgba(11, 11, 11, 0.8)"); this->setStyleSheet("color: #fff; background: rgba(11, 11, 11, 0.8)");
this->setAttribute(Qt::WA_TranslucentBackground); this->setAttribute(Qt::WA_TranslucentBackground);
@ -29,18 +29,10 @@ TooltipWidget::TooltipWidget(BaseWidget *parent)
this->setStayInScreenRect(true); this->setStayInScreenRect(true);
displayImage_->setAlignment(Qt::AlignHCenter); // Default to using vertical layout
displayImage_->setStyleSheet("background: transparent"); this->initializeVLayout();
this->setLayout(this->vLayout_);
displayText_->setAlignment(Qt::AlignHCenter); this->currentStyle_ = TooltipStyle::Vertical;
displayText_->setStyleSheet("background: transparent");
auto *layout = new QVBoxLayout(this);
layout->setSizeConstraint(QLayout::SetFixedSize);
layout->setContentsMargins(10, 5, 10, 5);
layout->addWidget(displayImage_);
layout->addWidget(displayText_);
this->setLayout(layout);
this->connections_.managedConnect(getFonts()->fontChanged, [this] { this->connections_.managedConnect(getFonts()->fontChanged, [this] {
this->updateFont(); this->updateFont();
@ -49,24 +41,219 @@ TooltipWidget::TooltipWidget(BaseWidget *parent)
auto windows = getApp()->windows; auto windows = getApp()->windows;
this->connections_.managedConnect(windows->gifRepaintRequested, [this] { this->connections_.managedConnect(windows->gifRepaintRequested, [this] {
if (this->image_ && this->image_->animated()) for (int i = 0; i < this->visibleEntries_; ++i)
{ {
this->refreshPixmap(); auto entry = this->entryAt(i);
if (entry && entry->animated())
{
entry->refreshPixmap();
}
} }
}); });
this->connections_.managedConnect(windows->miscUpdate, [this] { this->connections_.managedConnect(windows->miscUpdate, [this] {
if (this->image_ && this->attemptRefresh) bool needSizeAdjustment = false;
for (int i = 0; i < this->visibleEntries_; ++i)
{ {
if (this->refreshPixmap()) auto entry = this->entryAt(i);
if (entry->hasImage() && entry->attemptRefresh())
{
bool successfullyUpdated = entry->refreshPixmap();
needSizeAdjustment |= successfullyUpdated;
}
}
if (needSizeAdjustment)
{ {
this->attemptRefresh = false;
this->adjustSize(); this->adjustSize();
} }
}
}); });
} }
void TooltipWidget::setOne(const TooltipEntry &entry, TooltipStyle style)
{
this->set({entry}, style);
}
void TooltipWidget::set(const std::vector<TooltipEntry> &entries,
TooltipStyle style)
{
this->setCurrentStyle(style);
int delta = entries.size() - this->currentLayoutCount();
if (delta > 0)
{
// Need to add more TooltipEntry instances
int base = this->currentLayoutCount();
for (int i = 0; i < delta; ++i)
{
this->addNewEntry(base + i);
}
}
this->setVisibleEntries(entries.size());
for (int i = 0; i < entries.size(); ++i)
{
if (auto entryWidget = this->entryAt(i))
{
auto &entry = entries[i];
entryWidget->setImage(entry.image);
entryWidget->setText(entry.text);
entryWidget->setImageScale(entry.customWidth, entry.customHeight);
}
}
}
void TooltipWidget::setVisibleEntries(int n)
{
for (int i = 0; i < this->currentLayoutCount(); ++i)
{
auto *entry = this->entryAt(i);
if (entry == nullptr)
{
continue;
}
if (i >= n)
{
entry->hide();
entry->clearImage();
}
else
{
entry->show();
}
}
this->visibleEntries_ = n;
}
void TooltipWidget::addNewEntry(int absoluteIndex)
{
switch (this->currentStyle_)
{
case TooltipStyle::Vertical:
this->vLayout_->addWidget(new TooltipEntryWidget(),
Qt::AlignHCenter);
return;
case TooltipStyle::Grid:
if (absoluteIndex == 0)
{
// Top row spans all columns
this->gLayout_->addWidget(new TooltipEntryWidget(), 0, 0, 1,
GRID_NUM_COLS, Qt::AlignCenter);
}
else
{
int row = ((absoluteIndex - 1) / GRID_NUM_COLS) + 1;
int col = (absoluteIndex - 1) % GRID_NUM_COLS;
this->gLayout_->addWidget(new TooltipEntryWidget(), row, col,
Qt::AlignHCenter | Qt::AlignBottom);
}
return;
default:
return;
}
}
// May be nullptr
QLayout *TooltipWidget::currentLayout() const
{
switch (this->currentStyle_)
{
case TooltipStyle::Vertical:
return this->vLayout_;
case TooltipStyle::Grid:
return this->gLayout_;
default:
return nullptr;
}
}
int TooltipWidget::currentLayoutCount() const
{
if (auto *layout = this->currentLayout())
{
return layout->count();
}
return 0;
}
// May be nullptr
TooltipEntryWidget *TooltipWidget::entryAt(int n)
{
if (auto *layout = this->currentLayout())
{
return dynamic_cast<TooltipEntryWidget *>(layout->itemAt(n)->widget());
}
return nullptr;
}
void TooltipWidget::setCurrentStyle(TooltipStyle style)
{
if (this->currentStyle_ == style)
{
// Nothing to update
return;
}
this->clearEntries();
this->deleteCurrentLayout();
switch (style)
{
case TooltipStyle::Vertical:
this->initializeVLayout();
this->setLayout(this->vLayout_);
break;
case TooltipStyle::Grid:
this->initializeGLayout();
this->setLayout(this->gLayout_);
break;
default:
break;
}
this->currentStyle_ = style;
}
void TooltipWidget::deleteCurrentLayout()
{
auto *currentLayout = this->layout();
delete currentLayout;
switch (this->currentStyle_)
{
case TooltipStyle::Vertical:
this->vLayout_ = nullptr;
break;
case TooltipStyle::Grid:
this->gLayout_ = nullptr;
break;
default:
break;
}
}
void TooltipWidget::initializeVLayout()
{
auto *vLayout = new QVBoxLayout(this);
vLayout->setSizeConstraint(QLayout::SetFixedSize);
vLayout->setContentsMargins(10, 5, 10, 5);
vLayout->setSpacing(10);
this->vLayout_ = vLayout;
}
void TooltipWidget::initializeGLayout()
{
auto *gLayout = new QGridLayout(this);
gLayout->setSizeConstraint(QLayout::SetFixedSize);
gLayout->setContentsMargins(10, 5, 10, 5);
gLayout->setHorizontalSpacing(8);
gLayout->setVerticalSpacing(10);
this->gLayout_ = gLayout;
}
void TooltipWidget::themeChangedEvent() void TooltipWidget::themeChangedEvent()
{ {
// this->setStyleSheet("color: #fff; background: #000"); // this->setStyleSheet("color: #fff; background: #000");
@ -90,49 +277,26 @@ void TooltipWidget::updateFont()
getFonts()->getFont(FontStyle::ChatMediumSmall, this->scale())); getFonts()->getFont(FontStyle::ChatMediumSmall, this->scale()));
} }
void TooltipWidget::setText(QString text)
{
this->displayText_->setText(text);
}
void TooltipWidget::setWordWrap(bool wrap) void TooltipWidget::setWordWrap(bool wrap)
{ {
this->displayText_->setWordWrap(wrap); for (int i = 0; i < this->visibleEntries_; ++i)
{
auto entry = this->entryAt(i);
if (entry)
{
entry->setWordWrap(wrap);
}
}
} }
void TooltipWidget::clearImage() void TooltipWidget::clearEntries()
{ {
this->displayImage_->hide(); this->setVisibleEntries(0);
this->image_ = nullptr;
this->setImageScale(0, 0);
}
void TooltipWidget::setImage(ImagePtr image)
{
if (this->image_ == image)
{
return;
}
// hide image until loaded and reset scale
this->clearImage();
this->image_ = std::move(image);
this->refreshPixmap();
}
void TooltipWidget::setImageScale(int w, int h)
{
if (this->customImgWidth == w && this->customImgHeight == h)
{
return;
}
this->customImgWidth = w;
this->customImgHeight = h;
this->refreshPixmap();
} }
void TooltipWidget::hideEvent(QHideEvent *) void TooltipWidget::hideEvent(QHideEvent *)
{ {
this->clearImage(); this->clearEntries();
} }
void TooltipWidget::showEvent(QShowEvent *) void TooltipWidget::showEvent(QShowEvent *)
@ -140,34 +304,6 @@ void TooltipWidget::showEvent(QShowEvent *)
this->adjustSize(); this->adjustSize();
} }
bool TooltipWidget::refreshPixmap()
{
if (!this->image_)
{
return false;
}
auto pixmap = this->image_->pixmapOrLoad();
if (!pixmap)
{
this->attemptRefresh = true;
return false;
}
if (this->customImgWidth > 0 || this->customImgHeight > 0)
{
this->displayImage_->setPixmap(pixmap->scaled(
this->customImgWidth, this->customImgHeight, Qt::KeepAspectRatio));
}
else
{
this->displayImage_->setPixmap(*pixmap);
}
this->displayImage_->show();
return true;
}
void TooltipWidget::changeEvent(QEvent *) void TooltipWidget::changeEvent(QEvent *)
{ {
// clear parents event // clear parents event

View file

@ -1,9 +1,13 @@
#pragma once #pragma once
#include "widgets/BaseWindow.hpp" #include "widgets/BaseWindow.hpp"
#include "widgets/TooltipEntryWidget.hpp"
#include <pajlada/signals/signalholder.hpp> #include <pajlada/signals/signalholder.hpp>
#include <QGridLayout>
#include <QLabel> #include <QLabel>
#include <QLayout>
#include <QVBoxLayout>
#include <QWidget> #include <QWidget>
namespace chatterino { namespace chatterino {
@ -11,6 +15,15 @@ namespace chatterino {
class Image; class Image;
using ImagePtr = std::shared_ptr<Image>; using ImagePtr = std::shared_ptr<Image>;
struct TooltipEntry {
ImagePtr image;
QString text;
int customWidth = 0;
int customHeight = 0;
};
enum class TooltipStyle { Vertical, Grid };
class TooltipWidget : public BaseWindow class TooltipWidget : public BaseWindow
{ {
Q_OBJECT Q_OBJECT
@ -21,11 +34,13 @@ public:
TooltipWidget(BaseWidget *parent = nullptr); TooltipWidget(BaseWidget *parent = nullptr);
~TooltipWidget() override = default; ~TooltipWidget() override = default;
void setText(QString text); void setOne(const TooltipEntry &entry,
TooltipStyle style = TooltipStyle::Vertical);
void set(const std::vector<TooltipEntry> &entries,
TooltipStyle style = TooltipStyle::Vertical);
void setWordWrap(bool wrap); void setWordWrap(bool wrap);
void clearImage(); void clearEntries();
void setImage(ImagePtr image);
void setImageScale(int w, int h);
protected: protected:
void showEvent(QShowEvent *) override; void showEvent(QShowEvent *) override;
@ -39,17 +54,24 @@ protected:
private: private:
void updateFont(); void updateFont();
// used by WindowManager::gifRepaintRequested signal to progress frames when tooltip image is animated QLayout *currentLayout() const;
bool refreshPixmap(); int currentLayoutCount() const;
TooltipEntryWidget *entryAt(int n);
// set to true when tooltip image did not finish loading yet (pixmapOrLoad returned false) void setVisibleEntries(int n);
bool attemptRefresh{false}; void setCurrentStyle(TooltipStyle style);
void addNewEntry(int absoluteIndex);
void deleteCurrentLayout();
void initializeVLayout();
void initializeGLayout();
int visibleEntries_ = 0;
TooltipStyle currentStyle_;
QVBoxLayout *vLayout_;
QGridLayout *gLayout_;
ImagePtr image_ = nullptr;
int customImgWidth = 0;
int customImgHeight = 0;
QLabel *displayImage_;
QLabel *displayText_;
pajlada::Signals::SignalHolder connections_; pajlada::Signals::SignalHolder connections_;
}; };

View file

@ -493,7 +493,7 @@ void EmotePopup::filterTwitchEmotes(std::shared_ptr<Channel> searchChannel,
} }
if (!seventvGlobalEmotes.empty()) if (!seventvGlobalEmotes.empty())
{ {
addEmotes(*searchChannel, seventvGlobalEmotes, "SevenTV (Global)", addEmotes(*searchChannel, seventvGlobalEmotes, "7TV (Global)",
MessageElementFlag::SevenTVEmote); MessageElementFlag::SevenTVEmote);
} }
@ -522,7 +522,7 @@ void EmotePopup::filterTwitchEmotes(std::shared_ptr<Channel> searchChannel,
} }
if (!seventvChannelEmotes.empty()) if (!seventvChannelEmotes.empty())
{ {
addEmotes(*searchChannel, seventvChannelEmotes, "SevenTV (Channel)", addEmotes(*searchChannel, seventvChannelEmotes, "7TV (Channel)",
MessageElementFlag::SevenTVEmote); MessageElementFlag::SevenTVEmote);
} }
} }

View file

@ -149,7 +149,7 @@ void QuickSwitcherPopup::themeChangedEvent()
const QString selCol = const QString selCol =
(this->theme->isLightTheme() (this->theme->isLightTheme()
? "#68B1FF" // Copied from Theme::splits.input.styleSheet ? "#68B1FF" // Copied from Theme::splits.input.styleSheet
: this->theme->tabs.selected.backgrounds.regular.color().name()); : this->theme->tabs.selected.backgrounds.regular.name());
const QString listStyle = const QString listStyle =
QString( QString(

View file

@ -64,6 +64,7 @@
#define DRAW_WIDTH (this->width()) #define DRAW_WIDTH (this->width())
#define SELECTION_RESUME_SCROLLING_MSG_THRESHOLD 3 #define SELECTION_RESUME_SCROLLING_MSG_THRESHOLD 3
#define CHAT_HOVER_PAUSE_DURATION 1000 #define CHAT_HOVER_PAUSE_DURATION 1000
#define TOOLTIP_EMOTE_ENTRIES_LIMIT 7
namespace chatterino { namespace chatterino {
namespace { namespace {
@ -1658,10 +1659,12 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event)
auto element = &hoverLayoutElement->getCreator(); auto element = &hoverLayoutElement->getCreator();
bool isLinkValid = hoverLayoutElement->getLink().isValid(); bool isLinkValid = hoverLayoutElement->getLink().isValid();
auto emoteElement = dynamic_cast<const EmoteElement *>(element); auto emoteElement = dynamic_cast<const EmoteElement *>(element);
auto layeredEmoteElement =
dynamic_cast<const LayeredEmoteElement *>(element);
bool isNotEmote = emoteElement == nullptr && layeredEmoteElement == nullptr;
if (element->getTooltip().isEmpty() || if (element->getTooltip().isEmpty() ||
(isLinkValid && emoteElement == nullptr && (isLinkValid && isNotEmote && !getSettings()->linkInfoTooltip))
!getSettings()->linkInfoTooltip))
{ {
tooltipWidget->hide(); tooltipWidget->hide();
} }
@ -1669,7 +1672,7 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event)
{ {
auto badgeElement = dynamic_cast<const BadgeElement *>(element); auto badgeElement = dynamic_cast<const BadgeElement *>(element);
if ((badgeElement || emoteElement) && if ((badgeElement || emoteElement || layeredEmoteElement) &&
getSettings()->emotesTooltipPreview.getValue()) getSettings()->emotesTooltipPreview.getValue())
{ {
if (event->modifiers() == Qt::ShiftModifier || if (event->modifiers() == Qt::ShiftModifier ||
@ -1677,18 +1680,73 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event)
{ {
if (emoteElement) if (emoteElement)
{ {
tooltipWidget->setImage( tooltipWidget->setOne({
emoteElement->getEmote()->images.getImage(3.0)); emoteElement->getEmote()->images.getImage(3.0),
element->getTooltip(),
});
}
else if (layeredEmoteElement)
{
auto &layeredEmotes = layeredEmoteElement->getEmotes();
// Should never be empty but ensure it
if (!layeredEmotes.empty())
{
std::vector<TooltipEntry> entries;
entries.reserve(layeredEmotes.size());
auto &emoteTooltips =
layeredEmoteElement->getEmoteTooltips();
// Someone performing some tomfoolery could put an emote with tens,
// if not hundreds of zero-width emotes on a single emote. If the
// tooltip may take up more than three rows, truncate everything else.
bool truncating = false;
size_t upperLimit = layeredEmotes.size();
if (layeredEmotes.size() > TOOLTIP_EMOTE_ENTRIES_LIMIT)
{
upperLimit = TOOLTIP_EMOTE_ENTRIES_LIMIT - 1;
truncating = true;
}
for (size_t i = 0; i < upperLimit; ++i)
{
const auto &emote = layeredEmotes[i].ptr;
if (i == 0)
{
// First entry gets a large image and full description
entries.push_back({emote->images.getImage(3.0),
emoteTooltips[i]});
}
else
{
// Every other entry gets a small image and just the emote name
entries.push_back({emote->images.getImage(1.0),
emote->name.string});
}
}
if (truncating)
{
entries.push_back({nullptr, "..."});
}
auto style = layeredEmotes.size() > 2
? TooltipStyle::Grid
: TooltipStyle::Vertical;
tooltipWidget->set(entries, style);
}
} }
else if (badgeElement) else if (badgeElement)
{ {
tooltipWidget->setImage( tooltipWidget->setOne({
badgeElement->getEmote()->images.getImage(3.0)); badgeElement->getEmote()->images.getImage(3.0),
element->getTooltip(),
});
} }
} }
else else
{ {
tooltipWidget->clearImage(); tooltipWidget->clearEntries();
} }
} }
else else
@ -1711,7 +1769,7 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event)
auto thumbnailSize = getSettings()->thumbnailSize; auto thumbnailSize = getSettings()->thumbnailSize;
if (!thumbnailSize) if (!thumbnailSize)
{ {
tooltipWidget->clearImage(); tooltipWidget->clearEntries();
} }
else else
{ {
@ -1724,19 +1782,23 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event)
shouldHideThumbnail shouldHideThumbnail
? Image::fromResourcePixmap(getResources().streamerMode) ? Image::fromResourcePixmap(getResources().streamerMode)
: element->getThumbnail(); : element->getThumbnail();
tooltipWidget->setImage(std::move(thumb));
if (element->getThumbnailType() == if (element->getThumbnailType() ==
MessageElement::ThumbnailType::Link_Thumbnail) MessageElement::ThumbnailType::Link_Thumbnail)
{ {
tooltipWidget->setImageScale(thumbnailSize, thumbnailSize); tooltipWidget->setOne({std::move(thumb),
element->getTooltip(), thumbnailSize,
thumbnailSize});
}
else
{
tooltipWidget->setOne({std::move(thumb), ""});
} }
} }
} }
tooltipWidget->moveTo(this, event->globalPos()); tooltipWidget->moveTo(this, event->globalPos());
tooltipWidget->setWordWrap(isLinkValid); tooltipWidget->setWordWrap(isLinkValid);
tooltipWidget->setText(element->getTooltip());
tooltipWidget->show(); tooltipWidget->show();
} }
@ -2134,6 +2196,18 @@ void ChannelView::addImageContextMenuItems(
addEmoteContextMenuItems(*emoteElement->getEmote(), creatorFlags, addEmoteContextMenuItems(*emoteElement->getEmote(), creatorFlags,
menu); menu);
} }
else if (auto layeredElement =
dynamic_cast<const LayeredEmoteElement *>(&creator))
{
// Give each emote its own submenu
for (auto &emote : layeredElement->getUniqueEmotes())
{
auto emoteAction = menu.addAction(emote.ptr->name.string);
auto emoteMenu = new QMenu(&menu);
emoteAction->setMenu(emoteMenu);
addEmoteContextMenuItems(*emote.ptr, emote.flags, *emoteMenu);
}
}
} }
// add seperator // add seperator

View file

@ -51,12 +51,12 @@ void NotebookButton::paintEvent(QPaintEvent *event)
if (mouseDown_ || mouseOver_) if (mouseDown_ || mouseOver_)
{ {
background = this->theme->tabs.regular.backgrounds.hover.color(); background = this->theme->tabs.regular.backgrounds.hover;
foreground = this->theme->tabs.regular.text; foreground = this->theme->tabs.regular.text;
} }
else else
{ {
background = this->theme->tabs.regular.backgrounds.regular.color(); background = this->theme->tabs.regular.backgrounds.regular;
foreground = this->theme->tabs.regular.text; foreground = this->theme->tabs.regular.text;
} }

View file

@ -109,7 +109,7 @@ void GenericListView::refreshTheme(const Theme &theme)
const QString selCol = const QString selCol =
(theme.isLightTheme() (theme.isLightTheme()
? "#68B1FF" // Copied from Theme::splits.input.styleSheet ? "#68B1FF" // Copied from Theme::splits.input.styleSheet
: theme.tabs.selected.backgrounds.regular.color().name()); : theme.tabs.selected.backgrounds.regular.name());
const QString listStyle = const QString listStyle =
QString( QString(

View file

@ -374,6 +374,10 @@ void GeneralPage::initLayout(GeneralPageView &layout)
layout.addCheckbox("Animate", s.animateEmotes); layout.addCheckbox("Animate", s.animateEmotes);
layout.addCheckbox("Animate only when Chatterino is focused", layout.addCheckbox("Animate only when Chatterino is focused",
s.animationsWhenFocused); s.animationsWhenFocused);
layout.addCheckbox(
"Enable zero-width emotes", s.enableZeroWidthEmotes, false,
"When disabled, emotes that overlap other emotes, such as BTTV's "
"cvMask and 7TV's RainTime, will appear as normal emotes.");
layout.addCheckbox("Enable emote auto-completion by typing :", layout.addCheckbox("Enable emote auto-completion by typing :",
s.emoteCompletionWithColon); s.emoteCompletionWithColon);
layout.addDropdown<float>( layout.addDropdown<float>(

View file

@ -688,8 +688,7 @@ void SplitContainer::paintEvent(QPaintEvent * /*event*/)
rect.top() + rect.height() / 2 + (s / 2)); rect.top() + rect.height() / 2 + (s / 2));
} }
QBrush accentColor = auto accentColor = (QApplication::activeWindow() == this->window()
(QApplication::activeWindow() == this->window()
? this->theme->tabs.selected.backgrounds.regular ? this->theme->tabs.selected.backgrounds.regular
: this->theme->tabs.selected.backgrounds.unfocused); : this->theme->tabs.selected.backgrounds.unfocused);

View file

@ -960,8 +960,7 @@ void SplitHeader::enterEvent(QEvent *event)
} }
auto *tooltip = TooltipWidget::instance(); auto *tooltip = TooltipWidget::instance();
tooltip->clearImage(); tooltip->setOne({nullptr, this->tooltipText_});
tooltip->setText(this->tooltipText_);
tooltip->setWordWrap(true); tooltip->setWordWrap(true);
tooltip->adjustSize(); tooltip->adjustSize();
auto pos = this->mapToGlobal(this->rect().bottomLeft()) + auto pos = this->mapToGlobal(this->rect().bottomLeft()) +