diff --git a/BUILDING_ON_LINUX.md b/BUILDING_ON_LINUX.md new file mode 100644 index 00000000..06e02cd7 --- /dev/null +++ b/BUILDING_ON_LINUX.md @@ -0,0 +1,13 @@ +# Linux + +## Ubuntu 18.04 +*most likely works the same for other Debian-like distros* +1. Install dependencies (and the C++ IDE Qt Creator) `sudo apt install qtcreator qtmultimedia5-dev libqt5svg5-dev libboost-dev libssl-dev libboost-system-dev` +2. Open `chatterino.pro` with QT Creator and build + +## Arch Linux +install [chatterino2-git](https://aur.archlinux.org/packages/chatterino2-git/) from the aur or build manually as follows: +1. `sudo pacman -S qt5-base qt5-multimedia qt5-svg gst-plugins-ugly gst-plugins-good boost rapidjson` +2. go into project directory +3. create build folder `mkdir build && cd build` +4. `qmake .. && make` diff --git a/BUILDING_ON_MAC.md b/BUILDING_ON_MAC.md new file mode 100644 index 00000000..f4def52a --- /dev/null +++ b/BUILDING_ON_MAC.md @@ -0,0 +1,22 @@ +# Building on Mac OSX +1. install Xcode and Xcode Command Line Utilites +2. start Xcode, settings -> Locations, activate your Command Line Tools +3. install Qt Creator +4. install brew https://brew.sh/ +5. `brew install boost openssl rapidjson` +6. build the project using Qt Creator + +If the Project does not build at this point, you might need to add additional Paths/Libs, because brew does not install openssl and boost in the common path. You can get their path using + +`brew info openssl` +`brew info boost` + +The lines which you need to add to your project file should look similar to this + +``` +INCLUDEPATH += /usr/local/opt/openssl/include +LIBS += -L/usr/local/opt/openssl/lib + +INCLUDEPATH += "/usr/local/Cellar/boost/1.67.0_1/include" +LIBS += -L"/usr/local/Cellar/boost/1.67.0_1/lib" +``` diff --git a/BUILDING_ON_WINDOWS.md b/BUILDING_ON_WINDOWS.md new file mode 100644 index 00000000..aa1b7746 --- /dev/null +++ b/BUILDING_ON_WINDOWS.md @@ -0,0 +1,47 @@ +# Building on Windows (Recommended) +## Using Qt Creator +### Visual Studio 2017 +1. Install Visual Studio 2017 and select "Desktop development with C++" and "Universal Windows Platform development. + +### Boost +1. Visual Studio 2017 64-bit: https://dl.bintray.com/boostorg/release/1.66.0/binaries/boost_1_66_0-msvc-14.1-64.exe +2. When prompted, install boost to C:\local\boost +3. When the installation is finished, go to C:\local\boost and rename the "lib64-msvc-14.1" folder to "lib" + +### OpenSSL +#### For our websocket library, we need OpenSSL 1.1 +1. Download OpenSSL development library: https://slproweb.com/download/Win64OpenSSL-1_1_0h.exe +2. When prompted, install openssl to C:\local\openssl +3. When prompted, copy the OpenSSL DLLs to "The OpenSSL binaries (/bin) directory" +#### For Qt SSL, we need OpenSSL 1.0 +1. Download OpenSSL light: https://slproweb.com/download/Win64OpenSSL_Light-1_0_2o.exe +2. When prompted, install it anywhere +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 guide here if you don't know how to do it: https://www.computerhope.com/issues/ch000549.htm#windows8 ) + +### Qt +1. Download Qt: https://www.qt.io/download +2. Select "Open source" at the bottom of this page +3. Then select "Download" +#### When prompted which components to install: +1. Under the latest Qt version: + - Select MSVC 2017 64-bit (or MSVC 2015 64-bit if you still use Visual Studio 2015) + - Optionally, enable Qt WebEngine +2. Under Tools: + - Select Qt Creator, and Qt Creator CDB Debugger Support + +# Windows (Using MSYS2, not recommended) +Note: This guide is currently out of date and will not work as is. +Note: This build will have some features missing from the build. + +Building using MSYS2 can be quite easier process. Check out MSYS2 at [msys2.org](http://www.msys2.org/). + +Be sure to add `-j ` as a make argument so it will use all your cpu cores to build. [example setup](https://i.imgur.com/qlESlS1.png) + +You can also add `-o2` to optimize the final binary size but increase compilation time, and add `-pipe` to use more ram in compilation but increase compilation speed +1. open appropriate MSYS2 terminal and do `pacman -S mingw-w64--boost mingw-w64--qt5 mingw-w64--rapidjson` where `` is `x86_64` or `i686` +2. go into the project directory +3. create build folder `mkdir build && cd build` +4. `qmake .. && mingw32-make` diff --git a/README.md b/README.md index 21a39a34..02329cff 100644 --- a/README.md +++ b/README.md @@ -4,107 +4,27 @@ Chatterino 2 Chatterino 2 is the second installment of the Twitch chat client series "Chatterino". For now you can check out Chatterino 1 at [https://chatterino.com](https://chatterino.com). -## Code style -The code is normally formated using clang format in Qt Creator. [.clang-format](https://github.com/fourtf/chatterino2/blob/master/.clang-format) contains the style file for clang format. - -To setup automatic code formating with QT Creator, see [this guide](https://gist.github.com/pajlada/0296454198eb8f8789fd6fe7ea660c5b). - ## Building Before building run `git submodule update --init --recursive` to get required submodules. -### Windows -#### Using Qt Creator -##### Visual Studio 2017 -1. Install Visual Studio 2017 and select "Desktop development with C++" and "Universal Windows Platform development. +[Building on Windows](../master/BUILDING_ON_WINDOWS.md) -##### Boost -1. Visual Studio 2017 64-bit: https://dl.bintray.com/boostorg/release/1.66.0/binaries/boost_1_66_0-msvc-14.1-64.exe -2. When prompted, install boost to C:\local\boost -3. When the installation is finished, go to C:\local\boost and rename the "lib64-msvc-14.1" folder to "lib" +[Building on Linux](../master/BUILDING_ON_LINUX.md) -##### OpenSSL -###### For our websocket library, we need OpenSSL 1.1 -1. Download OpenSSL development library: https://slproweb.com/download/Win64OpenSSL-1_1_0h.exe -2. When prompted, install openssl to C:\local\openssl -3. When prompted, copy the OpenSSL DLLs to "The OpenSSL binaries (/bin) directory" -###### For Qt SSL, we need OpenSSL 1.0 -1. Download OpenSSL light: https://slproweb.com/download/Win64OpenSSL_Light-1_0_2o.exe -2. When prompted, install it anywhere -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 guide here if you don't know how to do it: https://www.computerhope.com/issues/ch000549.htm#windows8 ) +[Building on Mac](../master/BUILDING_ON_MAC.md) -##### Qt -1. Download Qt: https://www.qt.io/download -2. Select "Open source" at the bottom of this page -3. Then select "Download" -###### When prompted which components to install: -1. Under the latest Qt version: - - Select MSVC 2017 64-bit (or MSVC 2015 64-bit if you still use Visual Studio 2015) - - Optionally, enable Qt WebEngine -2. Under Tools: - - Select Qt Creator, and Qt Creator CDB Debugger Support +## Code style +The code is formated using clang format in Qt Creator. [.clang-format](https://github.com/fourtf/chatterino2/blob/master/.clang-format) contains the style file for clang format. +To setup automatic code formating with QT Creator, see [this guide](https://gist.github.com/pajlada/0296454198eb8f8789fd6fe7ea660c5b). -### Windows (Using MSYS2) -Note: This guide is currently out of date and will not work as is. -Note: This build will have some features missing from the build. +### Get it automated with QT Creator + Beautifier + Clang Format +1. Download LLVM: http://releases.llvm.org/5.0.1/LLVM-5.0.1-win64.exe +2. During the installation, make sure to add it to your path +3. In QT Creator, select `Help` > `About Plugins` > `C++` > `Beautifier` to enable the plugin +4. Restart QT Creator +5. Select `Tools` > `Options` > `Beautifier` +6. Under `General` select `Tool: ClangFormat` and enable `Automatic Formatting on File Save` +7. Under `Clang Format` select `Use predefined style: File` and `Fallback style: None` -Building using MSYS2 can be quite easier process. Check out MSYS2 at [msys2.org](http://www.msys2.org/). - -Be sure to add `-j ` as a make argument so it will use all your cpu cores to build. [example setup](https://i.imgur.com/qlESlS1.png) - -You can also add `-o2` to optimize the final binary size but increase compilation time, and add `-pipe` to use more ram in compilation but increase compilation speed -1. open appropriate MSYS2 terminal and do `pacman -S mingw-w64--boost mingw-w64--qt5 mingw-w64--rapidjson` where `` is `x86_64` or `i686` -2. go into the project directory -3. create build folder `mkdir build && cd build` -4. `qmake .. && mingw32-make` - -### - -### Linux -#### Ubuntu 16.04.2 LTS -*most likely works the same for other Debian-like distros* -1. install QT Creator `sudo apt-get install qtcreator qtmultimedia5-dev` -2. install boost-dev `sudo apt-get install libboost-dev` -3. open `chatterino.pro` with QT Creator and build - -#### Ubuntu 18.04 -*most likely works the same for other Debian-like distros* -1. Install dependencies (and the C++ IDE Qt Creator) `sudo apt install qtcreator qtmultimedia5-dev libqt5svg5-dev libboost-dev` -2. Install rapidjson to `/usr/local/` like this: From the Chatterino2 root folder: `sudo cp -r lib/rapidjson/include/rapidjson /usr/local/include`. If you want to install it to another place, you have to make sure it's in the chatterino.pro include path -3. open `chatterino.pro` with QT Creator and build - -#### Arch Linux -install [chatterino2-git](https://aur.archlinux.org/packages/chatterino2-git/) from the aur or build manually as follows: -1. `sudo pacman -S qt5-base qt5-multimedia qt5-svg gst-plugins-ugly gst-plugins-good boost rapidjson` -2. go into project directory -3. create build folder `mkdir build && cd build` -4. `qmake .. && make` - -### Mac OSX -1. install Xcode and Xcode Command Line Utilites -2. start Xcode, settings -> Locations, activate your Command Line Tools -3. install Qt Creator -4. install brew https://brew.sh/ -5. `brew install boost openssl rapidjson` -6. build the project using Qt Creator - -If the Project does not build at this point, you might need to add additional Paths/Libs, because brew does not install openssl and boost in the common path. You can get their path using - -`brew info openssl` -`brew info boost` - -The lines which you need to add to your project file should look similar to this - -``` -INCLUDEPATH += /usr/local/opt/openssl/include -LIBS += -L/usr/local/opt/openssl/lib - -INCLUDEPATH += "/usr/local/Cellar/boost/1.67.0_1/include" -LIBS += -L"/usr/local/Cellar/boost/1.67.0_1/lib" -``` - - -Test 1 +Qt creator should now format the documents when saving it. diff --git a/browser_ext/.editorconfig b/browser_ext/.editorconfig deleted file mode 100644 index 37346aa2..00000000 --- a/browser_ext/.editorconfig +++ /dev/null @@ -1,19 +0,0 @@ -root = true - -[*] - -# Change these settings to your own preference -indent_style = space -indent_size = 2 - -# We recommend you to keep these unchanged -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -[*.md] -trim_trailing_whitespace = false - -[Makefile] -indent_style = tab \ No newline at end of file diff --git a/browser_ext/background.js b/browser_ext/background.js deleted file mode 100644 index 5e34c3a4..00000000 --- a/browser_ext/background.js +++ /dev/null @@ -1,102 +0,0 @@ -const ignoredPages = { - "settings": true, - "payments": true, - "inventory": true, - "messages": true, - "subscriptions": true, - "friends": true, - "directory": true, -}; - -const appName = "com.chatterino.chatterino"; -let port = null; - - -/// Connect to port -function connectPort() { - port = chrome.runtime.connectNative("com.chatterino.chatterino"); - console.log("port connected"); - - port.onMessage.addListener(function (msg) { - console.log(msg); - }); - port.onDisconnect.addListener(function () { - console.log("port disconnected"); - - port = null; - }); -} - -function getPort() { - if (port) { - return port; - } else { - // TODO: add cooldown - connectPort(); - - return port; - } -} - - -/// Tab listeners -chrome.tabs.onActivated.addListener((activeInfo) => { - chrome.tabs.get(activeInfo.tabId, (tab) => { - if (!tab) - return; - - if (!tab.url) - return; - - matchUrl(tab.url, tab); - }); -}); - -chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { - if (!tab.highlighted) - return; - - matchUrl(changeInfo.url, tab); -}); - - -/// Misc -function matchUrl(url, tab) { - if (!url) - return; - - const match = url.match(/^https?:\/\/(www\.)?twitch.tv\/([a-zA-Z0-9]+)\/?$/); - - let channelName; - - console.log(tab); - - if (match && (channelName = match[2], !ignoredPages[channelName])) { - console.log("channelName " + channelName); - console.log("winId " + tab.windowId); - - chrome.windows.get(tab.windowId, {}, (window) => { - let yOffset = window.height - tab.height; - - let port = getPort(); - if (port) { - port.postMessage({ - action: "select", - attach: true, - type: "twitch", - name: channelName, - winId: "" + tab.windowId, - yOffset: yOffset - }); - } - }); - } else { - let port = getPort(); - if (port) { - port.postMessage({ - action: "detach", - winId: "" + tab.windowId - }) - } - } -} diff --git a/browser_ext/icon.png b/browser_ext/icon.png deleted file mode 100644 index 9a6f5ba1..00000000 Binary files a/browser_ext/icon.png and /dev/null differ diff --git a/browser_ext/manifest.json b/browser_ext/manifest.json deleted file mode 100644 index 74d9fae0..00000000 --- a/browser_ext/manifest.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "Chatterino", - "version": "1.0", - "description": "xd", - "permissions": [ - "tabs", "nativeMessaging" - ], - "icons": { - "256": "icon.png" - }, - "manifest_version": 2, - "background": { - "scripts": [ - "background.js" - ], - "persistent": false - }, - "browser_action": { - "default_popup": "popup.html" - } -} diff --git a/browser_ext/popup.html b/browser_ext/popup.html deleted file mode 100644 index e24ef44a..00000000 --- a/browser_ext/popup.html +++ /dev/null @@ -1,6 +0,0 @@ - - - - xd - - diff --git a/chatterino.pro b/chatterino.pro index 980bb2c5..309ce1a1 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -26,6 +26,11 @@ equals(QMAKE_CXX, "clang++")|equals(QMAKE_CXX, "g++") { macx:ICON = resources/images/chatterino2.icns win32:RC_FILE = resources/windows.rc + +macx { + LIBS += -L/usr/local/lib +} + # Submodules include(dependencies/rapidjson.pri) include(dependencies/settings.pri) @@ -39,10 +44,14 @@ include(dependencies/openssl.pri) include(dependencies/boost.pri) # Optional feature: QtWebEngine -exists ($(QTDIR)/include/QtWebEngine/QtWebEngine) { - message(Using QWebEngine) - QT += webenginewidgets - DEFINES += "USEWEBENGINE" +#exists ($(QTDIR)/include/QtWebEngine/QtWebEngine) { +# message(Using QWebEngine) +# QT += webenginewidgets +# DEFINES += "USEWEBENGINE" +#} + +linux { + LIBS += -lrt } win32 { @@ -105,7 +114,6 @@ SOURCES += \ src/providers/twitch/twitchmessagebuilder.cpp \ src/providers/twitch/twitchserver.cpp \ src/providers/twitch/pubsub.cpp \ - src/singletons/accountmanager.cpp \ src/singletons/commandmanager.cpp \ src/singletons/emotemanager.cpp \ src/singletons/fontmanager.cpp \ @@ -185,7 +193,6 @@ SOURCES += \ src/widgets/attachedwindow.cpp \ src/widgets/settingspages/externaltoolspage.cpp \ src/widgets/helper/comboboxitemdelegate.cpp \ - src/util/signalvectormodel.cpp \ src/controllers/commands/command.cpp \ src/controllers/commands/commandmodel.cpp \ src/controllers/commands/commandcontroller.cpp \ @@ -197,7 +204,15 @@ SOURCES += \ src/controllers/accounts/accountcontroller.cpp \ src/controllers/accounts/accountmodel.cpp \ src/controllers/accounts/account.cpp \ - src/widgets/helper/splitoverlay.cpp + src/widgets/helper/splitoverlay.cpp \ + src/widgets/helper/dropoverlay.cpp \ + src/widgets/helper/splitnode.cpp \ + src/widgets/notificationpopup.cpp \ + src/controllers/taggedusers/taggeduserscontroller.cpp \ + src/controllers/taggedusers/taggeduser.cpp \ + src/controllers/taggedusers/taggedusersmodel.cpp \ + src/util/emotemap.cpp \ + src/providers/irc/ircconnection2.cpp HEADERS += \ src/precompiled_header.hpp \ @@ -226,7 +241,6 @@ HEADERS += \ src/providers/twitch/twitchmessagebuilder.hpp \ src/providers/twitch/twitchserver.hpp \ src/providers/twitch/pubsub.hpp \ - src/singletons/accountmanager.hpp \ src/singletons/commandmanager.hpp \ src/singletons/emotemanager.hpp \ src/singletons/fontmanager.hpp \ @@ -343,7 +357,17 @@ HEADERS += \ src/controllers/accounts/accountmodel.hpp \ src/controllers/accounts/account.hpp \ src/util/sharedptrelementless.hpp \ - src/widgets/helper/splitoverlay.hpp + src/widgets/helper/splitoverlay.hpp \ + src/widgets/helper/dropoverlay.hpp \ + src/widgets/helper/splitnode.hpp \ + src/widgets/notificationpopup.hpp \ + src/controllers/taggedusers/taggeduserscontroller.hpp \ + src/controllers/taggedusers/taggeduser.hpp \ + src/providerid.hpp \ + src/controllers/taggedusers/taggedusersmodel.hpp \ + src/util/qstringhash.hpp \ + src/util/mutexvalue.hpp \ + src/providers/irc/ircconnection2.hpp RESOURCES += \ resources/resources.qrc diff --git a/resources/images/split/splitdown.png b/resources/images/split/splitdown.png index 3891ca39..042f1f4c 100644 Binary files a/resources/images/split/splitdown.png and b/resources/images/split/splitdown.png differ diff --git a/resources/images/split/splitleft.png b/resources/images/split/splitleft.png index 8d1cb0e1..210b74b4 100644 Binary files a/resources/images/split/splitleft.png and b/resources/images/split/splitleft.png differ diff --git a/resources/images/split/splitmove.png b/resources/images/split/splitmove.png index 82bf5382..e4ce85ce 100644 Binary files a/resources/images/split/splitmove.png and b/resources/images/split/splitmove.png differ diff --git a/resources/images/split/splitright.png b/resources/images/split/splitright.png index 17353733..88963fb3 100644 Binary files a/resources/images/split/splitright.png and b/resources/images/split/splitright.png differ diff --git a/resources/images/split/splitup.png b/resources/images/split/splitup.png index a9cc0f30..55662c9b 100644 Binary files a/resources/images/split/splitup.png and b/resources/images/split/splitup.png differ diff --git a/resources/licenses/boost_boost.txt b/resources/licenses/boost_boost.txt new file mode 100644 index 00000000..36b7cd93 --- /dev/null +++ b/resources/licenses/boost_boost.txt @@ -0,0 +1,23 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/resources/licenses/fmt_bsd2.txt b/resources/licenses/fmt_bsd2.txt new file mode 100644 index 00000000..eb6be650 --- /dev/null +++ b/resources/licenses/fmt_bsd2.txt @@ -0,0 +1,23 @@ +Copyright (c) 2012 - 2016, Victor Zverovich + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/resources/licenses/libcommuni_BSD3.txt b/resources/licenses/libcommuni_BSD3.txt new file mode 100644 index 00000000..0074060a --- /dev/null +++ b/resources/licenses/libcommuni_BSD3.txt @@ -0,0 +1,26 @@ +Copyright (C) 2008-2016 The Communi Project + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the {organization} nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/resources/licenses/openssl.txt b/resources/licenses/openssl.txt new file mode 100644 index 00000000..e953f590 --- /dev/null +++ b/resources/licenses/openssl.txt @@ -0,0 +1,125 @@ + + LICENSE ISSUES + ============== + + The OpenSSL toolkit stays under a double license, i.e. both the conditions of + the OpenSSL License and the original SSLeay license apply to the toolkit. + See below for the actual license texts. + + OpenSSL License + --------------- + +/* ==================================================================== + * Copyright (c) 1998-2018 The OpenSSL Project. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. All advertising materials mentioning features or use of this + * software must display the following acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit. (http://www.openssl.org/)" + * + * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please contact + * openssl-core@openssl.org. + * + * 5. Products derived from this software may not be called "OpenSSL" + * nor may "OpenSSL" appear in their names without prior written + * permission of the OpenSSL Project. + * + * 6. Redistributions of any form whatsoever must retain the following + * acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit (http://www.openssl.org/)" + * + * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY + * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * ==================================================================== + * + * This product includes cryptographic software written by Eric Young + * (eay@cryptsoft.com). This product includes software written by Tim + * Hudson (tjh@cryptsoft.com). + * + */ + + Original SSLeay License + ----------------------- + +/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) + * All rights reserved. + * + * This package is an SSL implementation written + * by Eric Young (eay@cryptsoft.com). + * The implementation was written so as to conform with Netscapes SSL. + * + * This library is free for commercial and non-commercial use as long as + * the following conditions are aheared to. The following conditions + * apply to all code found in this distribution, be it the RC4, RSA, + * lhash, DES, etc., code; not just the SSL code. The SSL documentation + * included with this distribution is covered by the same copyright terms + * except that the holder is Tim Hudson (tjh@cryptsoft.com). + * + * Copyright remains Eric Young's, and as such any Copyright notices in + * the code are not to be removed. + * If this package is used in a product, Eric Young should be given attribution + * as the author of the parts of the library used. + * This can be in the form of a textual message at program startup or + * in documentation (online or textual) provided with the package. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * "This product includes cryptographic software written by + * Eric Young (eay@cryptsoft.com)" + * The word 'cryptographic' can be left out if the rouines from the library + * being used are not cryptographic related :-). + * 4. If you include any Windows specific code (or a derivative thereof) from + * the apps directory (application code) you must include an acknowledgement: + * "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" + * + * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * The licence and distribution terms for any publically available version or + * derivative of this code cannot be changed. i.e. this code cannot simply be + * copied and put under another distribution licence + * [including the GNU Public Licence.] + */ + diff --git a/resources/licenses/pajlada_settings.txt b/resources/licenses/pajlada_settings.txt new file mode 100644 index 00000000..d2ea8ac5 --- /dev/null +++ b/resources/licenses/pajlada_settings.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 pajlada + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/resources/licenses/pajlada_signals.txt b/resources/licenses/pajlada_signals.txt new file mode 100644 index 00000000..d2ea8ac5 --- /dev/null +++ b/resources/licenses/pajlada_signals.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 pajlada + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/resources/licenses/qt_lgpl-3.0.txt b/resources/licenses/qt_lgpl-3.0.txt new file mode 100644 index 00000000..0a041280 --- /dev/null +++ b/resources/licenses/qt_lgpl-3.0.txt @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/resources/licenses/rapidjson.txt b/resources/licenses/rapidjson.txt new file mode 100644 index 00000000..7ccc161c --- /dev/null +++ b/resources/licenses/rapidjson.txt @@ -0,0 +1,57 @@ +Tencent is pleased to support the open source community by making RapidJSON available. + +Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. All rights reserved. + +If you have downloaded a copy of the RapidJSON binary from Tencent, please note that the RapidJSON binary is licensed under the MIT License. +If you have downloaded a copy of the RapidJSON source code from Tencent, please note that RapidJSON source code is licensed under the MIT License, except for the third-party components listed below which are subject to different license terms. Your integration of RapidJSON into your own projects may require compliance with the MIT License, as well as the other licenses applicable to the third-party components included within RapidJSON. To avoid the problematic JSON license in your own projects, it's sufficient to exclude the bin/jsonchecker/ directory, as it's the only code under the JSON license. +A copy of the MIT License is included in this file. + +Other dependencies and licenses: + +Open Source Software Licensed Under the BSD License: +-------------------------------------------------------------------- + +The msinttypes r29 +Copyright (c) 2006-2013 Alexander Chemeris +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +* Neither the name of copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Open Source Software Licensed Under the JSON License: +-------------------------------------------------------------------- + +json.org +Copyright (c) 2002 JSON.org +All Rights Reserved. + +JSON_checker +Copyright (c) 2002 JSON.org +All Rights Reserved. + + +Terms of the JSON License: +--------------------------------------------------- + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +Terms of the MIT License: +-------------------------------------------------------------------- + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/resources/licenses/websocketpp.txt b/resources/licenses/websocketpp.txt new file mode 100644 index 00000000..f8cc5ba1 --- /dev/null +++ b/resources/licenses/websocketpp.txt @@ -0,0 +1,145 @@ +Main Library: + +Copyright (c) 2014, Peter Thorson. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the WebSocket++ Project nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL PETER THORSON BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Bundled Libraries: + +****** Base 64 Library (base64/base64.hpp) ****** +base64.hpp is a repackaging of the base64.cpp and base64.h files into a +single header suitable for use as a header only library. This conversion was +done by Peter Thorson (webmaster@zaphoyd.com) in 2012. All modifications to +the code are redistributed under the same license as the original, which is +listed below. + +base64.cpp and base64.h + +Copyright (C) 2004-2008 René Nyffenegger + +This source code is provided 'as-is', without any express or implied +warranty. In no event will the author be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this source code must not be misrepresented; you must not + claim that you wrote the original source code. If you use this source code + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original source code. + +3. This notice may not be removed or altered from any source distribution. + +René Nyffenegger rene.nyffenegger@adp-gmbh.ch + +****** SHA1 Library (sha1/sha1.hpp) ****** +sha1.hpp is a repackaging of the sha1.cpp and sha1.h files from the shallsha1 +library (http://code.google.com/p/smallsha1/) into a single header suitable for +use as a header only library. This conversion was done by Peter Thorson +(webmaster@zaphoyd.com) in 2013. All modifications to the code are redistributed +under the same license as the original, which is listed below. + + Copyright (c) 2011, Micael Hildenborg + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Micael Hildenborg nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY Micael Hildenborg ''AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL Micael Hildenborg BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +****** MD5 Library (common/md5.hpp) ****** +md5.hpp is a reformulation of the md5.h and md5.c code from +http://www.opensource.apple.com/source/cups/cups-59/cups/md5.c to allow it to +function as a component of a header only library. This conversion was done by +Peter Thorson (webmaster@zaphoyd.com) in 2012 for the WebSocket++ project. The +changes are released under the same license as the original (listed below) + +Copyright (C) 1999, 2002 Aladdin Enterprises. All rights reserved. + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + +L. Peter Deutsch +ghost@aladdin.com + +****** UTF8 Validation logic (utf8_validation.hpp) ****** +utf8_validation.hpp is adapted from code originally written by Bjoern Hoehrmann +. See http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ for +details. + +The original license: + +Copyright (c) 2008-2009 Bjoern Hoehrmann + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/resources/resources.qrc b/resources/resources.qrc index aa3b7757..65c4bcc4 100644 --- a/resources/resources.qrc +++ b/resources/resources.qrc @@ -51,6 +51,15 @@ images/split/splitright.png images/split/splitup.png images/split/splitmove.png + licenses/boost_boost.txt + licenses/fmt_bsd2.txt + licenses/libcommuni_BSD3.txt + licenses/openssl.txt + licenses/pajlada_settings.txt + licenses/pajlada_signals.txt + licenses/qt_lgpl-3.0.txt + licenses/rapidjson.txt + licenses/websocketpp.txt qt.conf diff --git a/src/application.cpp b/src/application.cpp index 53746ff2..07178d52 100644 --- a/src/application.cpp +++ b/src/application.cpp @@ -1,11 +1,12 @@ #include "application.hpp" +#include "controllers/accounts/accountcontroller.hpp" #include "controllers/commands/commandcontroller.hpp" #include "controllers/highlights/highlightcontroller.hpp" #include "controllers/ignores/ignorecontroller.hpp" +#include "controllers/taggedusers/taggeduserscontroller.hpp" #include "providers/twitch/pubsub.hpp" #include "providers/twitch/twitchserver.hpp" -#include "singletons/accountmanager.hpp" #include "singletons/emotemanager.hpp" #include "singletons/fontmanager.hpp" #include "singletons/loggingmanager.hpp" @@ -19,12 +20,6 @@ #include -#ifdef Q_OS_WIN -#include -#include -#include -#endif - using namespace chatterino::singletons; namespace chatterino { @@ -34,7 +29,7 @@ namespace { bool isBigEndian() { int test = 1; - char *p = (char *)&test; + char *p = reinterpret_cast(&test); return p[0] == 0; } @@ -61,6 +56,7 @@ void Application::construct() isAppConstructed = true; // 1. Instantiate all classes + this->settings = new singletons::SettingManager; this->paths = new singletons::PathManager(this->argc, this->argv); this->themes = new singletons::ThemeManager; this->windows = new singletons::WindowManager; @@ -68,9 +64,9 @@ void Application::construct() this->commands = new controllers::commands::CommandController; this->highlights = new controllers::highlights::HighlightController; this->ignores = new controllers::ignores::IgnoreController; - this->accounts = new singletons::AccountManager; + this->taggedUsers = new controllers::taggedusers::TaggedUsersController; + this->accounts = new controllers::accounts::AccountController; this->emotes = new singletons::EmoteManager; - this->settings = new singletons::SettingManager; this->fonts = new singletons::FontManager; this->resources = new singletons::ResourceManager; @@ -92,12 +88,13 @@ void Application::initialize() // 2. Initialize/load classes this->settings->initialize(); - this->windows->initialize(); this->nativeMessaging->registerHost(); this->settings->load(); this->commands->load(); + this->logging->initialize(); + this->windows->initialize(); this->resources->initialize(); @@ -109,7 +106,6 @@ void Application::initialize() this->accounts->load(); this->twitch.server->initialize(); - this->logging->initialize(); // XXX this->settings->updateWordTypeMask(); @@ -182,8 +178,9 @@ void Application::initialize() } auto msg = messages::Message::createTimeoutMessage(action); + msg->flags |= messages::Message::PubSub; - util::postToThread([chan, msg] { chan->addMessage(msg); }); + util::postToThread([chan, msg] { chan->addOrReplaceTimeout(msg); }); }); this->twitch.pubsub->sig.moderation.userUnbanned.connect([&](const auto &action) { @@ -205,10 +202,10 @@ void Application::initialize() // TODO(pajlada): Unlisten to all authed topics instead of only moderation topics // this->twitch.pubsub->UnlistenAllAuthedTopics(); - this->twitch.pubsub->listenToWhispers(this->accounts->Twitch.getCurrent()); // + this->twitch.pubsub->listenToWhispers(this->accounts->twitch.getCurrent()); // }; - this->accounts->Twitch.currentUserChanged.connect(RequestModerationActions); + this->accounts->twitch.currentUserChanged.connect(RequestModerationActions); RequestModerationActions(); } @@ -231,52 +228,6 @@ void Application::save() this->commands->save(); } -void Application::runNativeMessagingHost() -{ - auto app = getApp(); - - app->nativeMessaging = new singletons::NativeMessagingManager; - -#ifdef Q_OS_WIN - _setmode(_fileno(stdin), _O_BINARY); - _setmode(_fileno(stdout), _O_BINARY); -#endif - -#if 0 - bool bigEndian = isBigEndian(); -#endif - - while (true) { - char size_c[4]; - std::cin.read(size_c, 4); - - if (std::cin.eof()) { - break; - } - - uint32_t size = *reinterpret_cast(size_c); -#if 0 - // To avoid breaking strict-aliasing rules and potentially inducing undefined behaviour, the following code can be run instead - uint32_t size = 0; - if (bigEndian) { - size = size_c[3] | static_cast(size_c[2]) << 8 | - static_cast(size_c[1]) << 16 | static_cast(size_c[0]) << 24; - } else { - size = size_c[0] | static_cast(size_c[1]) << 8 | - static_cast(size_c[2]) << 16 | static_cast(size_c[3]) << 24; - } -#endif - - char *b = (char *)malloc(size + 1); - std::cin.read(b, size); - *(b + size) = '\0'; - - app->nativeMessaging->sendToGuiProcess(QByteArray(b, size)); - - free(b); - } -} - Application *getApp() { assert(staticApp != nullptr); diff --git a/src/application.hpp b/src/application.hpp index ed13a808..ccb1ff2d 100644 --- a/src/application.hpp +++ b/src/application.hpp @@ -26,6 +26,12 @@ class HighlightController; namespace ignores { class IgnoreController; } +namespace taggedusers { +class TaggedUsersController; +} +namespace accounts { +class AccountController; +} } // namespace controllers namespace singletons { @@ -67,7 +73,8 @@ public: controllers::commands::CommandController *commands = nullptr; controllers::highlights::HighlightController *highlights = nullptr; controllers::ignores::IgnoreController *ignores = nullptr; - singletons::AccountManager *accounts = nullptr; + controllers::taggedusers::TaggedUsersController *taggedUsers = nullptr; + controllers::accounts::AccountController *accounts = nullptr; singletons::EmoteManager *emotes = nullptr; singletons::NativeMessagingManager *nativeMessaging = nullptr; singletons::SettingManager *settings = nullptr; diff --git a/src/channel.cpp b/src/channel.cpp index 2b718797..5c264c83 100644 --- a/src/channel.cpp +++ b/src/channel.cpp @@ -45,6 +45,11 @@ Channel::Type Channel::getType() const return this->type; } +bool Channel::isTwitchChannel() const +{ + return this->type >= Twitch && this->type < TwitchEnd; +} + bool Channel::isEmpty() const { return this->name.isEmpty(); @@ -60,57 +65,14 @@ void Channel::addMessage(MessagePtr message) auto app = getApp(); MessagePtr deleted; - bool isTimeout = (message->flags & Message::Timeout) != 0; - - if (!isTimeout) { - const QString &username = message->loginName; - if (!username.isEmpty()) { - // TODO: Add recent chatters display name. This should maybe be a setting - this->addRecentChatter(message); - } + const QString &username = message->loginName; + if (!username.isEmpty()) { + // TODO: Add recent chatters display name. This should maybe be a setting + this->addRecentChatter(message); } app->logging->addMessage(this->name, message); - if (isTimeout) { - LimitedQueueSnapshot snapshot = this->getMessageSnapshot(); - bool addMessage = true; - int snapshotLength = snapshot.getLength(); - - int end = std::max(0, snapshotLength - 20); - - for (int i = snapshotLength - 1; i >= end; --i) { - auto &s = snapshot[i]; - if (s->flags.HasFlag(Message::Untimeout) && s->timeoutUser == message->timeoutUser) { - break; - } - - if (s->flags.HasFlag(Message::Timeout) && s->timeoutUser == message->timeoutUser) { - assert(message->banAction != nullptr); - MessagePtr replacement( - Message::createTimeoutMessage(*(message->banAction), s->count + 1)); - this->replaceMessage(s, replacement); - addMessage = false; - } - } - - // disable the messages from the user - for (int i = 0; i < snapshotLength; i++) { - auto &s = snapshot[i]; - if ((s->flags & (Message::Timeout | Message::Untimeout)) == 0 && - s->loginName == message->timeoutUser) { - s->flags.EnableFlag(Message::Disabled); - } - } - - // XXX: Might need the following line - // WindowManager::getInstance().repaintVisibleChatWidgets(this); - - if (!addMessage) { - return; - } - } - if (this->messages.pushBack(message, deleted)) { this->messageRemovedFromStart.invoke(deleted); } @@ -118,6 +80,73 @@ void Channel::addMessage(MessagePtr message) this->messageAppended.invoke(message); } +void Channel::addOrReplaceTimeout(messages::MessagePtr message) +{ + LimitedQueueSnapshot snapshot = this->getMessageSnapshot(); + int snapshotLength = snapshot.getLength(); + + int end = std::max(0, snapshotLength - 20); + + bool addMessage = true; + + QTime minimumTime = QTime::currentTime().addSecs(-5); + + for (int i = snapshotLength - 1; i >= end; --i) { + auto &s = snapshot[i]; + + qDebug() << s->parseTime << minimumTime; + + if (s->parseTime < minimumTime) { + break; + } + + if (s->flags.HasFlag(Message::Untimeout) && s->timeoutUser == message->timeoutUser) { + break; + } + + if (s->flags.HasFlag(Message::Timeout) && s->timeoutUser == message->timeoutUser) { + if (message->flags.HasFlag(Message::PubSub) && !s->flags.HasFlag(Message::PubSub)) { + this->replaceMessage(s, message); + addMessage = false; + break; + } + if (!message->flags.HasFlag(Message::PubSub) && s->flags.HasFlag(Message::PubSub)) { + addMessage = false; + break; + } + + int count = s->count + 1; + + messages::MessagePtr replacement(Message::createSystemMessage( + message->searchText + QString("(") + QString::number(count) + " times)")); + + replacement->timeoutUser = message->timeoutUser; + replacement->count = count; + replacement->flags = message->flags; + + this->replaceMessage(s, replacement); + + return; + } + } + + // disable the messages from the user + for (int i = 0; i < snapshotLength; i++) { + auto &s = snapshot[i]; + if ((s->flags & (Message::Timeout | Message::Untimeout)) == 0 && + s->loginName == message->timeoutUser) { + s->flags.EnableFlag(Message::Disabled); + } + } + + if (addMessage) { + this->addMessage(message); + } + + // XXX: Might need the following line + // WindowManager::getInstance().repaintVisibleChatWidgets(this); +} + void Channel::addMessagesAtStart(std::vector &_messages) { std::vector addedMessages = this->messages.pushFront(_messages); diff --git a/src/channel.hpp b/src/channel.hpp index 6a2433f6..7b6f1715 100644 --- a/src/channel.hpp +++ b/src/channel.hpp @@ -29,6 +29,7 @@ public: TwitchWhispers, TwitchWatching, TwitchMentions, + TwitchEnd, }; explicit Channel(const QString &_name, Type type); @@ -43,11 +44,13 @@ public: pajlada::Signals::NoArgSignal destroyed; Type getType() const; + bool isTwitchChannel() const; virtual bool isEmpty() const; messages::LimitedQueueSnapshot getMessageSnapshot(); void addMessage(messages::MessagePtr message); void addMessagesAtStart(std::vector &messages); + void addOrReplaceTimeout(messages::MessagePtr message); void replaceMessage(messages::MessagePtr message, messages::MessagePtr replacement); virtual void addRecentChatter(const std::shared_ptr &message); diff --git a/src/common.hpp b/src/common.hpp index bba4fa46..c1762d7d 100644 --- a/src/common.hpp +++ b/src/common.hpp @@ -3,6 +3,7 @@ #include "debug/log.hpp" #include +#include #include #include @@ -20,4 +21,8 @@ inline QString qS(const std::string &string) return QString::fromStdString(string); } +const Qt::KeyboardModifiers showSplitOverlayModifiers = Qt::ControlModifier | Qt::AltModifier; +const Qt::KeyboardModifiers showAddSplitRegions = Qt::ControlModifier | Qt::AltModifier; +const Qt::KeyboardModifiers showResizeHandlesModifiers = Qt::ControlModifier; + } // namespace chatterino diff --git a/src/controllers/accounts/account.cpp b/src/controllers/accounts/account.cpp index a0dfc46b..e9ef921e 100644 --- a/src/controllers/accounts/account.cpp +++ b/src/controllers/accounts/account.cpp @@ -1,11 +1,23 @@ #include "account.hpp" +#include + namespace chatterino { namespace controllers { namespace accounts { -Account::Account(const QString &category) +Account::Account(ProviderId _providerId) + : providerId(_providerId) { + static QString twitch("Twitch"); + + this->category = [&]() { + switch (_providerId) { + case ProviderId::Twitch: + return twitch; + } + return QString("Unknown ProviderId"); + }(); } const QString &Account::getCategory() const @@ -13,16 +25,17 @@ const QString &Account::getCategory() const return this->category; } +ProviderId Account::getProviderId() const +{ + return this->providerId; +} + bool Account::operator<(const Account &other) const { - if (this->category < other.category) { - return true; - } else if (this->category == other.category) { - if (this->toString() < other.toString()) { - return true; - } - } - return false; + QString a = this->toString(); + QString b = other.toString(); + + return std::tie(this->category, a) < std::tie(other.category, b); } } // namespace accounts diff --git a/src/controllers/accounts/account.hpp b/src/controllers/accounts/account.hpp index 81b2652e..bab75c5d 100644 --- a/src/controllers/accounts/account.hpp +++ b/src/controllers/accounts/account.hpp @@ -1,5 +1,7 @@ #pragma once +#include "providerid.hpp" + #include namespace chatterino { @@ -9,14 +11,17 @@ namespace accounts { class Account { public: - Account(const QString &category); + Account(ProviderId providerId); + virtual ~Account() = default; virtual QString toString() const = 0; const QString &getCategory() const; + ProviderId getProviderId() const; bool operator<(const Account &other) const; private: + ProviderId providerId; QString category; }; diff --git a/src/controllers/accounts/accountcontroller.cpp b/src/controllers/accounts/accountcontroller.cpp index ce720435..c70d4587 100644 --- a/src/controllers/accounts/accountcontroller.cpp +++ b/src/controllers/accounts/accountcontroller.cpp @@ -8,13 +8,42 @@ namespace accounts { AccountController::AccountController() { + this->twitch.accounts.itemInserted.connect([this](const auto &args) { + this->accounts.insertItem(std::dynamic_pointer_cast(args.item)); + }); + + this->twitch.accounts.itemRemoved.connect([this](const auto &args) { + if (args.caller != this) { + auto &accs = this->twitch.accounts.getVector(); + auto it = std::find(accs.begin(), accs.end(), args.item); + assert(it != accs.end()); + + this->accounts.removeItem(it - accs.begin()); + } + }); + + this->accounts.itemRemoved.connect([this](const auto &args) { + switch (args.item->getProviderId()) { + case ProviderId::Twitch: { + auto &accs = this->twitch.accounts.getVector(); + auto it = std::find(accs.begin(), accs.end(), args.item); + assert(it != accs.end()); + + this->twitch.accounts.removeItem(it - accs.begin(), this); + } break; + } + }); +} + +void AccountController::load() +{ + this->twitch.load(); } AccountModel *AccountController::createModel(QObject *parent) { AccountModel *model = new AccountModel(parent); - //(util::BaseSignalVector *) model->init(&this->accounts); return model; } diff --git a/src/controllers/accounts/accountcontroller.hpp b/src/controllers/accounts/accountcontroller.hpp index 242cf767..585c2889 100644 --- a/src/controllers/accounts/accountcontroller.hpp +++ b/src/controllers/accounts/accountcontroller.hpp @@ -3,6 +3,7 @@ #include #include "controllers/accounts/account.hpp" +#include "providers/twitch/twitchaccountmanager.hpp" #include "util/sharedptrelementless.hpp" #include "util/signalvector2.hpp" @@ -19,6 +20,10 @@ public: AccountModel *createModel(QObject *parent); + void load(); + + providers::twitch::TwitchAccountManager twitch; + private: util::SortedSignalVector, util::SharedPtrElementLess> accounts; diff --git a/src/controllers/accounts/accountmodel.cpp b/src/controllers/accounts/accountmodel.cpp index 9d527570..f84a60e9 100644 --- a/src/controllers/accounts/accountmodel.cpp +++ b/src/controllers/accounts/accountmodel.cpp @@ -1,5 +1,7 @@ #include "accountmodel.hpp" +#include "util/standarditemhelper.hpp" + namespace chatterino { namespace controllers { namespace accounts { @@ -10,16 +12,49 @@ AccountModel::AccountModel(QObject *parent) } // turn a vector item into a model row -std::shared_ptr AccountModel::getItemFromRow(std::vector &row) +std::shared_ptr AccountModel::getItemFromRow(std::vector &, + const std::shared_ptr &original) { - return nullptr; + return original; } // turns a row in the model into a vector item void AccountModel::getRowFromItem(const std::shared_ptr &item, std::vector &row) { - row[0]->setData(item->toString(), Qt::DisplayRole); + util::setStringItem(row[0], item->toString(), false); + row[0]->setData(QFont("Segoe UI", 10), Qt::FontRole); +} + +int AccountModel::beforeInsert(const std::shared_ptr &item, + std::vector &row, int proposedIndex) +{ + if (this->categoryCount[item->getCategory()]++ == 0) { + auto row = this->createRow(); + + util::setStringItem(row[0], item->getCategory(), false, false); + row[0]->setData(QFont("Segoe UI Light", 16), Qt::FontRole); + + this->insertCustomRow(std::move(row), proposedIndex); + + return proposedIndex + 1; + } + + return proposedIndex; +} + +void AccountModel::afterRemoved(const std::shared_ptr &item, + std::vector &row, int index) +{ + auto it = this->categoryCount.find(item->getCategory()); + assert(it != this->categoryCount.end()); + + if (it->second <= 1) { + this->categoryCount.erase(it); + this->removeCustomRow(index - 1); + } else { + it->second--; + } } } // namespace accounts diff --git a/src/controllers/accounts/accountmodel.hpp b/src/controllers/accounts/accountmodel.hpp index 40b74750..0d2e1c78 100644 --- a/src/controllers/accounts/accountmodel.hpp +++ b/src/controllers/accounts/accountmodel.hpp @@ -1,8 +1,11 @@ #pragma once #include "controllers/accounts/account.hpp" +#include "util/qstringhash.hpp" #include "util/signalvectormodel.hpp" +#include + namespace chatterino { namespace controllers { namespace accounts { @@ -16,13 +19,23 @@ public: protected: // turn a vector item into a model row - virtual std::shared_ptr getItemFromRow(std::vector &row) override; + virtual std::shared_ptr getItemFromRow( + std::vector &row, const std::shared_ptr &original) override; // turns a row in the model into a vector item virtual void getRowFromItem(const std::shared_ptr &item, std::vector &row) override; + virtual int beforeInsert(const std::shared_ptr &item, + std::vector &row, int proposedIndex) override; + + virtual void afterRemoved(const std::shared_ptr &item, + std::vector &row, int index) override; + friend class AccountController; + +private: + std::unordered_map categoryCount; }; } // namespace accounts diff --git a/src/controllers/commands/commandcontroller.cpp b/src/controllers/commands/commandcontroller.cpp index 9b897f90..2640f359 100644 --- a/src/controllers/commands/commandcontroller.cpp +++ b/src/controllers/commands/commandcontroller.cpp @@ -1,12 +1,12 @@ #include "commandcontroller.hpp" #include "application.hpp" +#include "controllers/accounts/accountcontroller.hpp" #include "controllers/commands/command.hpp" #include "controllers/commands/commandmodel.hpp" #include "messages/messagebuilder.hpp" #include "providers/twitch/twitchchannel.hpp" #include "providers/twitch/twitchserver.hpp" -#include "singletons/accountmanager.hpp" #include "singletons/pathmanager.hpp" #include "util/signalvector2.hpp" @@ -110,7 +110,7 @@ QString CommandController::execCommand(const QString &text, ChannelPtr channel, return ""; } else if (commandName == "/uptime") { - const auto &streamStatus = twitchChannel->GetStreamStatus(); + const auto &streamStatus = twitchChannel->getStreamStatus(); QString messageText = streamStatus.live ? streamStatus.uptime : "Channel is not live."; @@ -121,7 +121,7 @@ QString CommandController::execCommand(const QString &text, ChannelPtr channel, } else if (commandName == "/ignore" && words.size() >= 2) { auto app = getApp(); - auto user = app->accounts->Twitch.getCurrent(); + auto user = app->accounts->twitch.getCurrent(); auto target = words.at(1); if (user->isAnon()) { @@ -138,7 +138,7 @@ QString CommandController::execCommand(const QString &text, ChannelPtr channel, } else if (commandName == "/unignore" && words.size() >= 2) { auto app = getApp(); - auto user = app->accounts->Twitch.getCurrent(); + auto user = app->accounts->twitch.getCurrent(); auto target = words.at(1); if (user->isAnon()) { @@ -161,7 +161,7 @@ QString CommandController::execCommand(const QString &text, ChannelPtr channel, messages::MessageBuilder b; - b.emplace(app->accounts->Twitch.getCurrent()->getUserName(), + b.emplace(app->accounts->twitch.getCurrent()->getUserName(), messages::MessageElement::Text); b.emplace("->", messages::MessageElement::Text); b.emplace(words[1], messages::MessageElement::Text); @@ -169,7 +169,7 @@ QString CommandController::execCommand(const QString &text, ChannelPtr channel, QString rest = ""; for (int i = 2; i < words.length(); i++) { - rest += words[i]; + rest += words[i] + " "; } b.emplace(rest, messages::MessageElement::Text); diff --git a/src/controllers/commands/commandmodel.cpp b/src/controllers/commands/commandmodel.cpp index d87ffacf..621e9d26 100644 --- a/src/controllers/commands/commandmodel.cpp +++ b/src/controllers/commands/commandmodel.cpp @@ -11,7 +11,7 @@ CommandModel::CommandModel(QObject *parent) } // turn a vector item into a model row -Command CommandModel::getItemFromRow(std::vector &row) +Command CommandModel::getItemFromRow(std::vector &row, const Command &original) { return Command(row[0]->data(Qt::EditRole).toString(), row[1]->data(Qt::EditRole).toString()); } diff --git a/src/controllers/commands/commandmodel.hpp b/src/controllers/commands/commandmodel.hpp index 22931c5b..7512c576 100644 --- a/src/controllers/commands/commandmodel.hpp +++ b/src/controllers/commands/commandmodel.hpp @@ -17,7 +17,8 @@ class CommandModel : public util::SignalVectorModel protected: // turn a vector item into a model row - virtual Command getItemFromRow(std::vector &row) override; + virtual Command getItemFromRow(std::vector &row, + const Command &command) override; // turns a row in the model into a vector item virtual void getRowFromItem(const Command &item, std::vector &row) override; diff --git a/src/controllers/highlights/highlightcontroller.cpp b/src/controllers/highlights/highlightcontroller.cpp index cab3cbbe..df10341e 100644 --- a/src/controllers/highlights/highlightcontroller.cpp +++ b/src/controllers/highlights/highlightcontroller.cpp @@ -2,6 +2,7 @@ #include "application.hpp" #include "controllers/highlights/highlightmodel.hpp" +#include "widgets/notificationpopup.hpp" namespace chatterino { namespace controllers { @@ -21,7 +22,6 @@ void HighlightController::initialize() } this->phrases.delayedItemsChanged.connect([this] { // - int xd = this->phrases.getVector().size(); this->highlightsSetting.setValue(this->phrases.getVector()); }); } @@ -34,6 +34,15 @@ HighlightModel *HighlightController::createModel(QObject *parent) return model; } +void HighlightController::addHighlight(const messages::MessagePtr &msg) +{ + // static widgets::NotificationPopup popup; + + // popup.updatePosition(); + // popup.addMessage(msg); + // popup.show(); +} + } // namespace highlights } // namespace controllers } // namespace chatterino diff --git a/src/controllers/highlights/highlightcontroller.hpp b/src/controllers/highlights/highlightcontroller.hpp index 98ad843d..e6bed334 100644 --- a/src/controllers/highlights/highlightcontroller.hpp +++ b/src/controllers/highlights/highlightcontroller.hpp @@ -1,6 +1,7 @@ #pragma once #include "controllers/highlights/highlightphrase.hpp" +#include "messages/message.hpp" #include "singletons/settingsmanager.hpp" #include "util/signalvector2.hpp" @@ -21,6 +22,8 @@ public: HighlightModel *createModel(QObject *parent); + void addHighlight(const messages::MessagePtr &msg); + private: bool initialized = false; diff --git a/src/controllers/highlights/highlightmodel.cpp b/src/controllers/highlights/highlightmodel.cpp index 280a205c..5dfbaf60 100644 --- a/src/controllers/highlights/highlightmodel.cpp +++ b/src/controllers/highlights/highlightmodel.cpp @@ -15,7 +15,8 @@ HighlightModel::HighlightModel(QObject *parent) } // turn a vector item into a model row -HighlightPhrase HighlightModel::getItemFromRow(std::vector &row) +HighlightPhrase HighlightModel::getItemFromRow(std::vector &row, + const HighlightPhrase &original) { // key, alert, sound, regex diff --git a/src/controllers/highlights/highlightmodel.hpp b/src/controllers/highlights/highlightmodel.hpp index f4a24fb9..84dd3fee 100644 --- a/src/controllers/highlights/highlightmodel.hpp +++ b/src/controllers/highlights/highlightmodel.hpp @@ -17,7 +17,8 @@ class HighlightModel : public util::SignalVectorModel protected: // turn a vector item into a model row - virtual HighlightPhrase getItemFromRow(std::vector &row) override; + virtual HighlightPhrase getItemFromRow(std::vector &row, + const HighlightPhrase &original) override; // turns a row in the model into a vector item virtual void getRowFromItem(const HighlightPhrase &item, diff --git a/src/controllers/ignores/ignoremodel.cpp b/src/controllers/ignores/ignoremodel.cpp index d8e59dc2..af38db8d 100644 --- a/src/controllers/ignores/ignoremodel.cpp +++ b/src/controllers/ignores/ignoremodel.cpp @@ -15,7 +15,8 @@ IgnoreModel::IgnoreModel(QObject *parent) } // turn a vector item into a model row -IgnorePhrase IgnoreModel::getItemFromRow(std::vector &row) +IgnorePhrase IgnoreModel::getItemFromRow(std::vector &row, + const IgnorePhrase &original) { // key, regex diff --git a/src/controllers/ignores/ignoremodel.hpp b/src/controllers/ignores/ignoremodel.hpp index a0bba4ec..3eacddfe 100644 --- a/src/controllers/ignores/ignoremodel.hpp +++ b/src/controllers/ignores/ignoremodel.hpp @@ -17,7 +17,8 @@ class IgnoreModel : public util::SignalVectorModel protected: // turn a vector item into a model row - virtual IgnorePhrase getItemFromRow(std::vector &row) override; + virtual IgnorePhrase getItemFromRow(std::vector &row, + const IgnorePhrase &original) override; // turns a row in the model into a vector item virtual void getRowFromItem(const IgnorePhrase &item, diff --git a/src/controllers/taggedusers/taggeduser.cpp b/src/controllers/taggedusers/taggeduser.cpp new file mode 100644 index 00000000..6f50957b --- /dev/null +++ b/src/controllers/taggedusers/taggeduser.cpp @@ -0,0 +1,24 @@ +#include "taggeduser.hpp" + +#include + +namespace chatterino { +namespace controllers { +namespace taggedusers { + +TaggedUser::TaggedUser(ProviderId _provider, const QString &_name, const QString &_id) + : provider(_provider) + , name(_name) + , id(_id) +{ +} + +bool TaggedUser::operator<(const TaggedUser &other) const +{ + return std::tie(this->provider, this->name, this->id) < + std::tie(other.provider, other.name, other.id); +} + +} // namespace taggedusers +} // namespace controllers +} // namespace chatterino diff --git a/src/controllers/taggedusers/taggeduser.hpp b/src/controllers/taggedusers/taggeduser.hpp new file mode 100644 index 00000000..3a8d3b17 --- /dev/null +++ b/src/controllers/taggedusers/taggeduser.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +namespace chatterino { +namespace controllers { +namespace taggedusers { + +class TaggedUser +{ +public: + TaggedUser(ProviderId provider, const QString &name, const QString &id); + + bool operator<(const TaggedUser &other) const; + + ProviderId provider; + QString name; + QString id; +}; + +} // namespace taggedusers +} // namespace controllers +} // namespace chatterino diff --git a/src/controllers/taggedusers/taggeduserscontroller.cpp b/src/controllers/taggedusers/taggeduserscontroller.cpp new file mode 100644 index 00000000..98486c1f --- /dev/null +++ b/src/controllers/taggedusers/taggeduserscontroller.cpp @@ -0,0 +1,23 @@ +#include "taggeduserscontroller.hpp" + +#include "controllers/taggedusers/taggedusersmodel.hpp" + +namespace chatterino { +namespace controllers { +namespace taggedusers { + +TaggedUsersController::TaggedUsersController() +{ +} + +TaggedUsersModel *TaggedUsersController::createModel(QObject *parent) +{ + TaggedUsersModel *model = new TaggedUsersModel(parent); + model->init(&this->users); + + return model; +} + +} // namespace taggedusers +} // namespace controllers +} // namespace chatterino diff --git a/src/controllers/taggedusers/taggeduserscontroller.hpp b/src/controllers/taggedusers/taggeduserscontroller.hpp new file mode 100644 index 00000000..85e845c8 --- /dev/null +++ b/src/controllers/taggedusers/taggeduserscontroller.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "controllers/taggedusers/taggeduser.hpp" +#include "util/signalvector2.hpp" + +namespace chatterino { +namespace controllers { +namespace taggedusers { + +class TaggedUsersModel; + +class TaggedUsersController +{ +public: + TaggedUsersController(); + + util::SortedSignalVector> users; + + TaggedUsersModel *createModel(QObject *parent = nullptr); +}; + +} // namespace taggedusers +} // namespace controllers +} // namespace chatterino diff --git a/src/controllers/taggedusers/taggedusersmodel.cpp b/src/controllers/taggedusers/taggedusersmodel.cpp new file mode 100644 index 00000000..565bdf2e --- /dev/null +++ b/src/controllers/taggedusers/taggedusersmodel.cpp @@ -0,0 +1,66 @@ +#include "taggedusersmodel.hpp" + +#include "application.hpp" +#include "util/standarditemhelper.hpp" + +namespace chatterino { +namespace controllers { +namespace taggedusers { + +// commandmodel +TaggedUsersModel::TaggedUsersModel(QObject *parent) + : util::SignalVectorModel(1, parent) +{ +} + +// turn a vector item into a model row +TaggedUser TaggedUsersModel::getItemFromRow(std::vector &row, + const TaggedUser &original) +{ + return original; +} + +// turns a row in the model into a vector item +void TaggedUsersModel::getRowFromItem(const TaggedUser &item, std::vector &row) +{ + util::setStringItem(row[0], item.name); +} + +void TaggedUsersModel::afterInit() +{ + // std::vector row = this->createRow(); + // util::setBoolItem(row[0], getApp()->settings->enableHighlightsSelf.getValue(), true, + // false); row[0]->setData("Your username (automatic)", Qt::DisplayRole); + // util::setBoolItem(row[1], getApp()->settings->enableHighlightTaskbar.getValue(), true, + // false); util::setBoolItem(row[2], getApp()->settings->enableHighlightSound.getValue(), + // true, false); row[3]->setFlags(0); this->insertCustomRow(row, 0); +} + +// void TaggedUserModel::customRowSetData(const std::vector &row, int column, +// const QVariant &value, int role) +//{ +// switch (column) { +// case 0: { +// if (role == Qt::CheckStateRole) { +// getApp()->settings->enableHighlightsSelf.setValue(value.toBool()); +// } +// } break; +// case 1: { +// if (role == Qt::CheckStateRole) { +// getApp()->settings->enableHighlightTaskbar.setValue(value.toBool()); +// } +// } break; +// case 2: { +// if (role == Qt::CheckStateRole) { +// getApp()->settings->enableHighlightSound.setValue(value.toBool()); +// } +// } break; +// case 3: { +// // empty element +// } break; +// } +//} + +} // namespace taggedusers +} // namespace controllers +} // namespace chatterino diff --git a/src/controllers/taggedusers/taggedusersmodel.hpp b/src/controllers/taggedusers/taggedusersmodel.hpp new file mode 100644 index 00000000..6db822c2 --- /dev/null +++ b/src/controllers/taggedusers/taggedusersmodel.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "controllers/taggedusers/taggeduser.hpp" +#include "util/signalvectormodel.hpp" + +namespace chatterino { +namespace controllers { +namespace taggedusers { + +class TaggedUsersController; + +class TaggedUsersModel : public util::SignalVectorModel +{ + explicit TaggedUsersModel(QObject *parent); + +protected: + // turn a vector item into a model row + virtual TaggedUser getItemFromRow(std::vector &row, + const TaggedUser &original) override; + + // turns a row in the model into a vector item + virtual void getRowFromItem(const TaggedUser &item, std::vector &row) override; + + virtual void afterInit() override; + + // virtual void customRowSetData(const std::vector &row, int column, + // const QVariant &value, int role) override; + + friend class TaggedUsersController; +}; + +} // namespace taggedusers +} // namespace controllers +} // namespace chatterino diff --git a/src/main.cpp b/src/main.cpp index 3c372094..c7249734 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,7 @@ #include "application.hpp" #include "singletons/nativemessagingmanager.hpp" #include "singletons/pathmanager.hpp" +#include "singletons/updatemanager.hpp" #include "util/networkmanager.hpp" #include "widgets/lastruncrashdialog.hpp" @@ -9,6 +10,7 @@ #include #include #include +#include #ifdef USEWINSDK #include "util/nativeeventhelper.hpp" @@ -17,7 +19,15 @@ #include #include +#ifdef Q_OS_WIN +#include +#include +#include +#endif + int runGui(int argc, char *argv[]); +void runNativeMessagingHost(); +void installCustomPalette(); int main(int argc, char *argv[]) { @@ -30,7 +40,7 @@ int main(int argc, char *argv[]) // TODO: can be any argument if (args.size() > 0 && args[0].startsWith("chrome-extension://")) { - chatterino::Application::runNativeMessagingHost(); + runNativeMessagingHost(); return 0; } @@ -47,14 +57,16 @@ int runGui(int argc, char *argv[]) // QApplication::setAttribute(Qt::AA_UseSoftwareOpenGL, true); QApplication a(argc, argv); -// Install native event handler for hidpi on windows -#ifdef USEWINSDK - a.installNativeEventFilter(new chatterino::util::DpiNativeEventFilter); -#endif + QApplication::setStyle(QStyleFactory::create("Fusion")); + + installCustomPalette(); // Initialize NetworkManager chatterino::util::NetworkManager::init(); + // Check for upates + chatterino::singletons::UpdateManager::getInstance().checkForUpdates(); + // Initialize application chatterino::Application::instantiate(argc, argv); auto app = chatterino::getApp(); @@ -68,7 +80,14 @@ int runGui(int argc, char *argv[]) if (QFile::exists(runningPath)) { #ifndef DISABLE_CRASH_DIALOG chatterino::widgets::LastRunCrashDialog dialog; - dialog.exec(); + + switch (dialog.exec()) { + case QDialog::Accepted: { + }; break; + default: { + _exit(0); + } + } #endif } else { QFile runningFile(runningPath); @@ -98,3 +117,86 @@ int runGui(int argc, char *argv[]) _exit(0); } + +void runNativeMessagingHost() +{ + auto *nm = new chatterino::singletons::NativeMessagingManager; + +#ifdef Q_OS_WIN + _setmode(_fileno(stdin), _O_BINARY); + _setmode(_fileno(stdout), _O_BINARY); +#endif + +#if 0 + bool bigEndian = isBigEndian(); +#endif + + std::atomic ping(false); + + QTimer timer; + QObject::connect(&timer, &QTimer::timeout, [&ping] { + if (!ping.exchange(false)) { + _exit(0); + } + }); + timer.setInterval(11000); + timer.start(); + + while (true) { + char size_c[4]; + std::cin.read(size_c, 4); + + if (std::cin.eof()) { + break; + } + + uint32_t size = *reinterpret_cast(size_c); +#if 0 + // To avoid breaking strict-aliasing rules and potentially inducing undefined behaviour, the following code can be run instead + uint32_t size = 0; + if (bigEndian) { + size = size_c[3] | static_cast(size_c[2]) << 8 | + static_cast(size_c[1]) << 16 | static_cast(size_c[0]) << 24; + } else { + size = size_c[0] | static_cast(size_c[1]) << 8 | + static_cast(size_c[2]) << 16 | static_cast(size_c[3]) << 24; + } +#endif + + std::unique_ptr b(new char[size + 1]); + std::cin.read(b.get(), size); + *(b.get() + size) = '\0'; + + nm->sendToGuiProcess(QByteArray::fromRawData(b.get(), static_cast(size))); + } +} + +void installCustomPalette() +{ + // borrowed from + // https://stackoverflow.com/questions/15035767/is-the-qt-5-dark-fusion-theme-available-for-windows + QPalette darkPalette = qApp->palette(); + + darkPalette.setColor(QPalette::Window, QColor(22, 22, 22)); + darkPalette.setColor(QPalette::WindowText, Qt::white); + darkPalette.setColor(QPalette::Text, Qt::white); + darkPalette.setColor(QPalette::Disabled, QPalette::WindowText, QColor(127, 127, 127)); + darkPalette.setColor(QPalette::Base, QColor("#333")); + darkPalette.setColor(QPalette::AlternateBase, QColor("#444")); + darkPalette.setColor(QPalette::ToolTipBase, Qt::white); + darkPalette.setColor(QPalette::ToolTipText, Qt::white); + darkPalette.setColor(QPalette::Disabled, QPalette::Text, QColor(127, 127, 127)); + darkPalette.setColor(QPalette::Dark, QColor(35, 35, 35)); + darkPalette.setColor(QPalette::Shadow, QColor(20, 20, 20)); + darkPalette.setColor(QPalette::Button, QColor(70, 70, 70)); + darkPalette.setColor(QPalette::ButtonText, Qt::white); + darkPalette.setColor(QPalette::Disabled, QPalette::ButtonText, QColor(127, 127, 127)); + darkPalette.setColor(QPalette::BrightText, Qt::red); + darkPalette.setColor(QPalette::Link, QColor(42, 130, 218)); + darkPalette.setColor(QPalette::Highlight, QColor(42, 130, 218)); + darkPalette.setColor(QPalette::Disabled, QPalette::Highlight, QColor(80, 80, 80)); + darkPalette.setColor(QPalette::HighlightedText, Qt::white); + darkPalette.setColor(QPalette::Disabled, QPalette::HighlightedText, QColor(127, 127, 127)); + + qApp->setPalette(darkPalette); +} diff --git a/src/messages/image.cpp b/src/messages/image.cpp index aec60a57..62c13721 100644 --- a/src/messages/image.cpp +++ b/src/messages/image.cpp @@ -147,10 +147,10 @@ void Image::loadImage() loadedEventQueued = true; QTimer::singleShot(500, [] { - getApp()->emotes->incGeneration(); + getApp()->windows->incGeneration(); auto app = getApp(); - app->windows->layoutVisibleChatWidgets(); + app->windows->layoutChannelViews(); loadedEventQueued = false; }); } @@ -240,7 +240,8 @@ int Image::getWidth() const int Image::getScaledWidth() const { - return static_cast(this->getWidth() * this->scale); + return static_cast((float)this->getWidth() * this->scale * + getApp()->settings->emoteScale.getValue()); } int Image::getHeight() const @@ -253,7 +254,8 @@ int Image::getHeight() const int Image::getScaledHeight() const { - return static_cast(this->getHeight() * this->scale); + return static_cast((float)this->getHeight() * this->scale * + getApp()->settings->emoteScale.getValue()); } } // namespace messages diff --git a/src/messages/layouts/messagelayout.cpp b/src/messages/layouts/messagelayout.cpp index 96151114..3e690e06 100644 --- a/src/messages/layouts/messagelayout.cpp +++ b/src/messages/layouts/messagelayout.cpp @@ -3,6 +3,8 @@ #include "application.hpp" #include "singletons/emotemanager.hpp" #include "singletons/settingsmanager.hpp" +#include "singletons/windowmanager.hpp" +#include "util/benchmark.hpp" #include #include @@ -20,9 +22,9 @@ namespace chatterino { namespace messages { namespace layouts { -MessageLayout::MessageLayout(MessagePtr _message) - : message(_message) - , buffer(nullptr) +MessageLayout::MessageLayout(MessagePtr message) + : message_(message) + , buffer_(nullptr) { util::DebugCount::increase("message layout"); } @@ -34,119 +36,116 @@ MessageLayout::~MessageLayout() Message *MessageLayout::getMessage() { - return this->message.get(); + return this->message_.get(); } // Height int MessageLayout::getHeight() const { - return container.getHeight(); + return container_.getHeight(); } // Layout // return true if redraw is required bool MessageLayout::layout(int width, float scale, MessageElement::Flags flags) { + // BenchmarkGuard benchmark("MessageLayout::layout()"); + auto app = getApp(); bool layoutRequired = false; // check if width changed - bool widthChanged = width != this->currentLayoutWidth; + bool widthChanged = width != this->currentLayoutWidth_; layoutRequired |= widthChanged; - this->currentLayoutWidth = width; + this->currentLayoutWidth_ = width; - // check if emotes changed - bool imagesChanged = this->emoteGeneration != app->emotes->getGeneration(); - layoutRequired |= imagesChanged; - this->emoteGeneration = app->emotes->getGeneration(); - - // check if text changed - bool textChanged = this->fontGeneration != app->fonts->getGeneration(); - layoutRequired |= textChanged; - this->fontGeneration = app->fonts->getGeneration(); + // check if layout state changed + if (this->layoutState_ != app->windows->getGeneration()) { + layoutRequired = true; + this->flags |= RequiresBufferUpdate; + this->layoutState_ = app->windows->getGeneration(); + } // check if work mask changed - bool wordMaskChanged = this->currentWordFlags != flags; // app->settings->getWordTypeMask(); - layoutRequired |= wordMaskChanged; - this->currentWordFlags = flags; // app->settings->getWordTypeMask(); + layoutRequired |= this->currentWordFlags_ != flags; + this->currentWordFlags_ = flags; // app->settings->getWordTypeMask(); - // check if timestamp format changed - bool timestampFormatChanged = this->timestampFormat != app->settings->timestampFormat; - - layoutRequired |= timestampFormatChanged; + // check if layout was requested manually + layoutRequired |= bool(this->flags & RequiresLayout); + this->flags &= decltype(RequiresLayout)(~RequiresLayout); // check if dpi changed - bool scaleChanged = this->scale != scale; - layoutRequired |= scaleChanged; - this->scale = scale; - imagesChanged |= scaleChanged; - textChanged |= scaleChanged; + layoutRequired |= this->scale_ != scale; + this->scale_ = scale; - // update word sizes if needed - if (imagesChanged) { - // this->container.updateImages(); - this->flags |= MessageLayout::RequiresBufferUpdate; - } - if (textChanged) { - // this->container.updateText(); - this->flags |= MessageLayout::RequiresBufferUpdate; - } - if (widthChanged || wordMaskChanged) { - this->deleteBuffer(); - } - - // return if no layout is required if (!layoutRequired) { return false; } + int oldHeight = this->container_.getHeight(); this->actuallyLayout(width, flags); + if (widthChanged || this->container_.getHeight() != oldHeight) { + this->deleteBuffer(); + } this->invalidateBuffer(); return true; } -void MessageLayout::actuallyLayout(int width, MessageElement::Flags flags) +void MessageLayout::actuallyLayout(int width, MessageElement::Flags _flags) { - this->container.begin(width, this->scale, this->message->flags.value); + auto messageFlags = this->message_->flags.value; - for (const std::unique_ptr &element : this->message->getElements()) { - element->addToContainer(this->container, flags); + if (this->flags & MessageLayout::Expanded || + (_flags & MessageElement::ModeratorTools && + !(this->message_->flags & Message::MessageFlags::Disabled))) { + messageFlags = Message::MessageFlags(messageFlags & ~Message::MessageFlags::Collapsed); } - if (this->height != this->container.getHeight()) { + this->container_.begin(width, this->scale_, messageFlags); + + for (const std::unique_ptr &element : this->message_->getElements()) { + element->addToContainer(this->container_, _flags); + } + + if (this->height_ != this->container_.getHeight()) { this->deleteBuffer(); } - this->container.end(); - this->height = this->container.getHeight(); + this->container_.end(); + this->height_ = this->container_.getHeight(); + + // collapsed state + this->flags &= ~Flags::Collapsed; + if (this->container_.isCollapsed()) { + this->flags |= Flags::Collapsed; + } } // Painting -void MessageLayout::paint(QPainter &painter, int y, int messageIndex, Selection &selection, - bool isLastReadMessage, bool isWindowFocused) +void MessageLayout::paint(QPainter &painter, int width, int y, int messageIndex, + Selection &selection, bool isLastReadMessage, bool isWindowFocused) { auto app = getApp(); - QPixmap *pixmap = this->buffer.get(); + QPixmap *pixmap = this->buffer_.get(); // create new buffer if required if (!pixmap) { #ifdef Q_OS_MACOS - pixmap = - new QPixmap((int)(this->container.getWidth() * painter.device()->devicePixelRatioF()), - (int)(this->container.getHeight() * painter.device()->devicePixelRatioF())); + pixmap = new QPixmap(int(width * painter.device()->devicePixelRatioF()), + int(container_.getHeight() * painter.device()->devicePixelRatioF())); pixmap->setDevicePixelRatio(painter.device()->devicePixelRatioF()); #else - pixmap = new QPixmap(this->container.getWidth(), std::max(16, this->container.getHeight())); + pixmap = new QPixmap(width, std::max(16, this->container_.getHeight())); #endif - this->buffer = std::shared_ptr(pixmap); - this->bufferValid = false; + this->buffer_ = std::shared_ptr(pixmap); + this->bufferValid_ = false; util::DebugCount::increase("message drawing buffers"); } - if (!this->bufferValid || !selection.isEmpty()) { + if (!this->bufferValid_ || !selection.isEmpty()) { this->updateBuffer(pixmap, messageIndex, selection); } @@ -155,21 +154,21 @@ void MessageLayout::paint(QPainter &painter, int y, int messageIndex, Selection // painter.drawPixmap(0, y, this->container.width, this->container.getHeight(), *pixmap); // draw gif emotes - this->container.paintAnimatedElements(painter, y); + this->container_.paintAnimatedElements(painter, y); // draw disabled - if (this->message->flags.HasFlag(Message::Disabled)) { + if (this->message_->flags.HasFlag(Message::Disabled)) { painter.fillRect(0, y, pixmap->width(), pixmap->height(), app->themes->messages.disabled); } // draw selection if (!selection.isEmpty()) { - this->container.paintSelection(painter, messageIndex, selection, y); + this->container_.paintSelection(painter, messageIndex, selection, y); } // draw message seperation line if (app->settings->seperateMessages.getValue()) { - painter.fillRect(0, y + this->container.getHeight() - 1, this->container.getWidth(), 1, + painter.fillRect(0, y, this->container_.getWidth(), 1, app->themes->splits.messageSeperator); } @@ -180,14 +179,13 @@ void MessageLayout::paint(QPainter &painter, int y, int messageIndex, Selection QBrush brush(color, Qt::VerPattern); - painter.fillRect(0, y + this->container.getHeight() - 1, this->container.getWidth(), 1, - brush); + painter.fillRect(0, y + this->container_.getHeight() - 1, pixmap->width(), 1, brush); } - this->bufferValid = true; + this->bufferValid_ = true; } -void MessageLayout::updateBuffer(QPixmap *buffer, int messageIndex, Selection &selection) +void MessageLayout::updateBuffer(QPixmap *buffer, int /*messageIndex*/, Selection & /*selection*/) { auto app = getApp(); @@ -197,8 +195,10 @@ void MessageLayout::updateBuffer(QPixmap *buffer, int messageIndex, Selection &s // draw background QColor backgroundColor; - if (this->message->flags & Message::Highlighted) { + if (this->message_->flags & Message::Highlighted) { backgroundColor = app->themes->messages.backgrounds.highlighted; + } else if (this->message_->flags & Message::Subscription) { + backgroundColor = app->themes->messages.backgrounds.subscription; } else if (app->settings->alternateMessageBackground.getValue() && this->flags & MessageLayout::AlternateBackground) { backgroundColor = app->themes->messages.backgrounds.alternate; @@ -208,7 +208,7 @@ void MessageLayout::updateBuffer(QPixmap *buffer, int messageIndex, Selection &s painter.fillRect(buffer->rect(), backgroundColor); // draw message - this->container.paintElements(painter); + this->container_.paintElements(painter); #ifdef FOURTF // debug @@ -226,15 +226,15 @@ void MessageLayout::updateBuffer(QPixmap *buffer, int messageIndex, Selection &s void MessageLayout::invalidateBuffer() { - this->bufferValid = false; + this->bufferValid_ = false; } void MessageLayout::deleteBuffer() { - if (this->buffer != nullptr) { + if (this->buffer_ != nullptr) { util::DebugCount::decrease("message drawing buffers"); - this->buffer = nullptr; + this->buffer_ = nullptr; } } @@ -243,7 +243,7 @@ void MessageLayout::deleteCache() this->deleteBuffer(); #ifdef XD - this->container.clear(); + this->container_.clear(); #endif } @@ -256,22 +256,22 @@ void MessageLayout::deleteCache() const MessageLayoutElement *MessageLayout::getElementAt(QPoint point) { // go through all words and return the first one that contains the point. - return this->container.getElementAt(point); + return this->container_.getElementAt(point); } int MessageLayout::getLastCharacterIndex() const { - return this->container.getLastCharacterIndex(); + return this->container_.getLastCharacterIndex(); } int MessageLayout::getSelectionIndex(QPoint position) { - return this->container.getSelectionIndex(position); + return this->container_.getSelectionIndex(position); } void MessageLayout::addSelectionText(QString &str, int from, int to) { - this->container.addSelectionText(str, from, to); + this->container_.addSelectionText(str, from, to); } } // namespace layouts diff --git a/src/messages/layouts/messagelayout.hpp b/src/messages/layouts/messagelayout.hpp index 76878c07..7b3c074a 100644 --- a/src/messages/layouts/messagelayout.hpp +++ b/src/messages/layouts/messagelayout.hpp @@ -22,10 +22,12 @@ public: enum Flags : uint8_t { RequiresBufferUpdate = 1 << 1, RequiresLayout = 1 << 2, - AlternateBackground = 1 << 3 + AlternateBackground = 1 << 3, + Collapsed = 1 << 4, + Expanded = 1 << 5, }; - MessageLayout(MessagePtr message); + MessageLayout(MessagePtr message_); ~MessageLayout(); Message *getMessage(); @@ -37,10 +39,10 @@ public: util::FlagsEnum flags; // Layout - bool layout(int width, float scale, MessageElement::Flags flags); + bool layout(int width, float scale_, MessageElement::Flags flags); // Painting - void paint(QPainter &painter, int y, int messageIndex, Selection &selection, + void paint(QPainter &painter, int width, int y, int messageIndex, Selection &selection, bool isLastReadMessage, bool isWindowFocused); void invalidateBuffer(); void deleteBuffer(); @@ -50,30 +52,28 @@ public: const MessageLayoutElement *getElementAt(QPoint point); int getLastCharacterIndex() const; int getSelectionIndex(QPoint position); - void addSelectionText(QString &str, int from, int to); + void addSelectionText(QString &str, int from = 0, int to = INT_MAX); // Misc bool isDisabled() const; private: // variables - MessagePtr message; - MessageLayoutContainer container; - std::shared_ptr buffer = nullptr; - bool bufferValid = false; + MessagePtr message_; + MessageLayoutContainer container_; + std::shared_ptr buffer_ = nullptr; + bool bufferValid_ = false; - int height = 0; + int height_ = 0; - int currentLayoutWidth = -1; - int fontGeneration = -1; - int emoteGeneration = -1; - QString timestampFormat; - float scale = -1; - unsigned int bufferUpdatedCount = 0; + int currentLayoutWidth_ = -1; + int layoutState_ = -1; + float scale_ = -1; + unsigned int bufferUpdatedCount_ = 0; - MessageElement::Flags currentWordFlags = MessageElement::None; + MessageElement::Flags currentWordFlags_ = MessageElement::None; - int collapsedHeight = 32; + int collapsedHeight_ = 32; // methods void actuallyLayout(int width, MessageElement::Flags flags); diff --git a/src/messages/layouts/messagelayoutcontainer.cpp b/src/messages/layouts/messagelayoutcontainer.cpp index dbb1ee9c..58aeeca8 100644 --- a/src/messages/layouts/messagelayoutcontainer.cpp +++ b/src/messages/layouts/messagelayoutcontainer.cpp @@ -9,6 +9,7 @@ #include #define COMPACT_EMOTES_OFFSET 6 +#define MAX_UNCOLLAPSED_LINES (getApp()->settings->collpseMessagesMinLines.getValue()) namespace chatterino { namespace messages { @@ -36,6 +37,12 @@ void MessageLayoutContainer::begin(int _width, float _scale, Message::MessageFla this->width = _width; this->scale = _scale; this->flags = _flags; + auto mediumFontMetrics = getApp()->fonts->getFontMetrics(FontStyle::ChatMedium, _scale); + this->textLineHeight = mediumFontMetrics.height(); + this->spaceWidth = mediumFontMetrics.width(' '); + this->dotdotdotWidth = mediumFontMetrics.width("..."); + this->_canAddMessages = true; + this->_isCollapsed = false; } void MessageLayoutContainer::clear() @@ -68,12 +75,12 @@ void MessageLayoutContainer::addElementNoLineBreak(MessageLayoutElement *element bool MessageLayoutContainer::canAddElements() { - return !(this->flags & Message::MessageFlags::Collapsed && line >= 3); + return this->_canAddMessages; } -void MessageLayoutContainer::_addElement(MessageLayoutElement *element) +void MessageLayoutContainer::_addElement(MessageLayoutElement *element, bool forceAdd) { - if (!this->canAddElements()) { + if (!this->canAddElements() && !forceAdd) { delete element; return; } @@ -129,6 +136,11 @@ void MessageLayoutContainer::breakLine() yExtra = (COMPACT_EMOTES_OFFSET / 2) * this->scale; } + // if (element->getCreator().getFlags() & MessageElement::Badges) { + if (element->getRect().height() < this->textLineHeight) { + yExtra -= (this->textLineHeight - element->getRect().height()) / 2; + } + element->setPosition(QPoint(element->getRect().x() + xOffset + this->margin.left, element->getRect().y() + this->lineHeight + yExtra)); } @@ -146,6 +158,12 @@ void MessageLayoutContainer::breakLine() this->lineStart = this->elements.size(); // this->currentX = (int)(this->scale * 8); + + if (this->canCollapse() && line + 1 >= MAX_UNCOLLAPSED_LINES) { + this->_canAddMessages = false; + return; + } + this->currentX = 0; this->currentY += this->lineHeight; this->height = this->currentY + (this->margin.bottom * this->scale); @@ -160,15 +178,32 @@ bool MessageLayoutContainer::atStartOfLine() bool MessageLayoutContainer::fitsInLine(int _width) { - return this->currentX + _width <= this->width - this->margin.left - this->margin.right; + return this->currentX + _width <= + (this->width - this->margin.left - this->margin.right - + (this->line + 1 == MAX_UNCOLLAPSED_LINES ? this->dotdotdotWidth : 0)); } void MessageLayoutContainer::end() { + if (!this->canAddElements()) { + static TextElement dotdotdot("...", MessageElement::Collapsed, MessageColor::Link); + static QString dotdotdotText("..."); + + auto *element = new TextLayoutElement( + dotdotdot, dotdotdotText, QSize(this->dotdotdotWidth, this->textLineHeight), + QColor("#00D80A"), FontStyle::ChatMediumBold, this->scale); + + // getApp()->themes->messages.textColors.system + this->_addElement(element, true); + this->_isCollapsed = true; + } + if (!this->atStartOfLine()) { this->breakLine(); } + this->height += this->lineHeight; + if (this->lines.size() != 0) { this->lines[0].rect.setTop(-100000); this->lines.back().rect.setBottom(100000); @@ -177,6 +212,17 @@ void MessageLayoutContainer::end() } } +bool MessageLayoutContainer::canCollapse() +{ + return getApp()->settings->collpseMessagesMinLines.getValue() > 0 && + this->flags & Message::MessageFlags::Collapsed; +} + +bool MessageLayoutContainer::isCollapsed() +{ + return this->_isCollapsed; +} + MessageLayoutElement *MessageLayoutContainer::getElementAt(QPoint point) { for (std::unique_ptr &element : this->elements) { @@ -429,8 +475,6 @@ void MessageLayoutContainer::addSelectionText(QString &str, int from, int to) for (std::unique_ptr &ele : this->elements) { int c = ele->getSelectionIndexCount(); - qDebug() << c; - if (first) { if (index + c > from) { ele->addCopyTextToString(str, from - index, to - index); diff --git a/src/messages/layouts/messagelayoutcontainer.hpp b/src/messages/layouts/messagelayoutcontainer.hpp index 6400b789..91e4df1d 100644 --- a/src/messages/layouts/messagelayoutcontainer.hpp +++ b/src/messages/layouts/messagelayoutcontainer.hpp @@ -76,6 +76,8 @@ struct MessageLayoutContainer { int getLastCharacterIndex() const; void addSelectionText(QString &str, int from, int to); + bool isCollapsed(); + private: struct Line { int startIndex; @@ -86,7 +88,7 @@ private: }; // helpers - void _addElement(MessageLayoutElement *element); + void _addElement(MessageLayoutElement *element, bool forceAdd = false); // variables float scale = 1.f; @@ -100,6 +102,12 @@ private: size_t lineStart = 0; int lineHeight = 0; int spaceWidth = 4; + int textLineHeight = 0; + int dotdotdotWidth = 0; + bool _canAddMessages = true; + bool _isCollapsed = false; + + bool canCollapse(); std::vector> elements; std::vector lines; diff --git a/src/messages/layouts/messagelayoutelement.cpp b/src/messages/layouts/messagelayoutelement.cpp index d2c0d2cc..788e23ee 100644 --- a/src/messages/layouts/messagelayoutelement.cpp +++ b/src/messages/layouts/messagelayoutelement.cpp @@ -188,7 +188,7 @@ int TextLayoutElement::getMouseOverIndex(const QPoint &abs) auto app = getApp(); - QFontMetrics &metrics = app->fonts->getFontMetrics(this->style, this->scale); + QFontMetrics metrics = app->fonts->getFontMetrics(this->style, this->scale); int x = this->getRect().left(); @@ -209,7 +209,7 @@ int TextLayoutElement::getXFromIndex(int index) { auto app = getApp(); - QFontMetrics &metrics = app->fonts->getFontMetrics(this->style, this->scale); + QFontMetrics metrics = app->fonts->getFontMetrics(this->style, this->scale); if (index <= 0) { return this->getRect().left(); diff --git a/src/messages/message.cpp b/src/messages/message.cpp index 38ea0837..5d0b1355 100644 --- a/src/messages/message.cpp +++ b/src/messages/message.cpp @@ -21,6 +21,8 @@ SBHighlight Message::getScrollBarHighlight() const { if (this->flags & Message::Highlighted) { return SBHighlight(SBHighlight::Highlight); + } else if (this->flags & Message::Subscription) { + return SBHighlight(SBHighlight::Subscription); } return SBHighlight(); } @@ -32,7 +34,19 @@ MessagePtr Message::createSystemMessage(const QString &text) message->addElement(new TimestampElement(QTime::currentTime())); message->addElement(new TextElement(text, MessageElement::Text, MessageColor::System)); - message->flags.EnableFlag(MessageFlags::System); + message->flags |= MessageFlags::System; + message->flags |= MessageFlags::DoNotTriggerNotification; + message->searchText = text; + + return message; +} + +MessagePtr Message::createMessage(const QString &text) +{ + MessagePtr message(new Message); + + message->addElement(new TimestampElement(QTime::currentTime())); + message->addElement(new TextElement(text, MessageElement::Text, MessageColor::Text)); message->searchText = text; return message; @@ -63,7 +77,7 @@ MessagePtr Message::createTimeoutMessage(const QString &username, const QString if (reason.length() > 0) { text.append(": \""); - text.append(util::ParseTagString(reason)); + text.append(util::parseTagString(reason)); text.append("\""); } text.append("."); @@ -89,7 +103,6 @@ MessagePtr Message::createTimeoutMessage(const providers::twitch::BanAction &act msg->timeoutUser = action.target.name; msg->count = count; - msg->banAction.reset(new providers::twitch::BanAction(action)); QString text; diff --git a/src/messages/message.hpp b/src/messages/message.hpp index d3f81367..7e95eb45 100644 --- a/src/messages/message.hpp +++ b/src/messages/message.hpp @@ -18,6 +18,7 @@ namespace messages { struct Message { Message() + : parseTime(QTime::currentTime()) { util::DebugCount::increase("messages"); } @@ -39,6 +40,8 @@ struct Message { Collapsed = (1 << 7), DisconnectedMessage = (1 << 8), Untimeout = (1 << 9), + PubSub = (1 << 10), + Subscription = (1 << 11), }; util::FlagsEnum flags; @@ -50,7 +53,6 @@ struct Message { QString localizedName; QString timeoutUser; - std::unique_ptr banAction; uint32_t count = 1; // Messages should not be added after the message is done initializing. @@ -65,6 +67,7 @@ private: public: static std::shared_ptr createSystemMessage(const QString &text); + static std::shared_ptr createMessage(const QString &text); static std::shared_ptr createTimeoutMessage(const QString &username, const QString &durationInSeconds, diff --git a/src/messages/messageelement.cpp b/src/messages/messageelement.cpp index dc288ed3..8b1f4c73 100644 --- a/src/messages/messageelement.cpp +++ b/src/messages/messageelement.cpp @@ -70,8 +70,8 @@ ImageElement::ImageElement(Image *_image, MessageElement::Flags flags) void ImageElement::addToContainer(MessageLayoutContainer &container, MessageElement::Flags _flags) { if (_flags & this->getFlags()) { - QSize size(this->image->getWidth() * this->image->getScale() * container.getScale(), - this->image->getHeight() * this->image->getScale() * container.getScale()); + QSize size(this->image->getScaledWidth() * container.getScale(), + this->image->getScaledHeight() * container.getScale()); container.addElement( (new ImageLayoutElement(*this, this->image, size))->setLink(this->getLink())); @@ -97,19 +97,10 @@ void EmoteElement::addToContainer(MessageLayoutContainer &container, MessageElem return; } - int quality = getApp()->settings->preferredEmoteQuality; + Image *_image = this->data.getImage(container.getScale()); - Image *_image; - if (quality == 3 && this->data.image3x != nullptr) { - _image = this->data.image3x; - } else if (quality >= 2 && this->data.image2x != nullptr) { - _image = this->data.image2x; - } else { - _image = this->data.image1x; - } - - QSize size((int)(container.getScale() * _image->getScaledWidth()), - (int)(container.getScale() * _image->getScaledHeight())); + QSize size(int(container.getScale() * _image->getScaledWidth()), + int(container.getScale() * _image->getScaledHeight())); container.addElement( (new ImageLayoutElement(*this, _image, size))->setLink(this->getLink())); @@ -139,7 +130,7 @@ void TextElement::addToContainer(MessageLayoutContainer &container, MessageEleme auto app = getApp(); if (_flags & this->getFlags()) { - QFontMetrics &metrics = app->fonts->getFontMetrics(this->style, container.getScale()); + QFontMetrics metrics = app->fonts->getFontMetrics(this->style, container.getScale()); for (Word &word : this->words) { auto getTextLayoutElement = [&](QString text, int width, bool trailingSpace) { @@ -242,7 +233,7 @@ TextElement *TimestampElement::formatTime(const QTime &time) QString format = locale.toString(time, getApp()->settings->timestampFormat); - return new TextElement(format, Flags::Timestamp, MessageColor::System, FontStyle::Medium); + return new TextElement(format, Flags::Timestamp, MessageColor::System, FontStyle::ChatMedium); } // TWITCH MODERATION diff --git a/src/messages/messageelement.hpp b/src/messages/messageelement.hpp index 2f281c12..fc2be592 100644 --- a/src/messages/messageelement.hpp +++ b/src/messages/messageelement.hpp @@ -158,7 +158,7 @@ class TextElement : public MessageElement public: TextElement(const QString &text, MessageElement::Flags flags, const MessageColor &color = MessageColor::Text, - FontStyle style = FontStyle::Medium); + FontStyle style = FontStyle::ChatMedium); ~TextElement() override = default; void addToContainer(MessageLayoutContainer &container, MessageElement::Flags flags) override; @@ -169,7 +169,6 @@ public: // b) which size it wants class EmoteElement : public MessageElement { - const util::EmoteData data; std::unique_ptr textElement; public: @@ -177,6 +176,8 @@ public: ~EmoteElement() override = default; void addToContainer(MessageLayoutContainer &container, MessageElement::Flags flags) override; + + const util::EmoteData data; }; // contains a text, formated depending on the preferences diff --git a/src/messages/messageparseargs.hpp b/src/messages/messageparseargs.hpp index e9b0b4db..4196f655 100644 --- a/src/messages/messageparseargs.hpp +++ b/src/messages/messageparseargs.hpp @@ -7,6 +7,7 @@ struct MessageParseArgs { bool disablePingSounds = false; bool isReceivedWhisper = false; bool isSentWhisper = false; + bool trimSubscriberUsername = false; }; } // namespace messages diff --git a/src/messages/selection.hpp b/src/messages/selection.hpp index 62a3ba6a..805f48a8 100644 --- a/src/messages/selection.hpp +++ b/src/messages/selection.hpp @@ -42,6 +42,11 @@ struct SelectionItem { { return this->messageIndex == b.messageIndex && this->charIndex == b.charIndex; } + + bool operator!=(const SelectionItem &b) const + { + return this->operator==(b); + } }; struct Selection { diff --git a/src/providerid.hpp b/src/providerid.hpp new file mode 100644 index 00000000..1ec190a3 --- /dev/null +++ b/src/providerid.hpp @@ -0,0 +1,5 @@ +#pragma once + +namespace chatterino { +enum class ProviderId { Twitch }; +} diff --git a/src/providers/irc/abstractircserver.cpp b/src/providers/irc/abstractircserver.cpp index 070fd5e2..52e97e56 100644 --- a/src/providers/irc/abstractircserver.cpp +++ b/src/providers/irc/abstractircserver.cpp @@ -4,6 +4,8 @@ #include "messages/limitedqueuesnapshot.hpp" #include "messages/message.hpp" +#include + using namespace chatterino::messages; namespace chatterino { @@ -13,14 +15,14 @@ namespace irc { AbstractIrcServer::AbstractIrcServer() { // Initialize the connections - this->writeConnection.reset(new Communi::IrcConnection); + this->writeConnection.reset(new IrcConnection); this->writeConnection->moveToThread(QCoreApplication::instance()->thread()); QObject::connect(this->writeConnection.get(), &Communi::IrcConnection::messageReceived, [this](auto msg) { this->writeConnectionMessageReceived(msg); }); // Listen to read connection message signals - this->readConnection.reset(new Communi::IrcConnection); + this->readConnection.reset(new IrcConnection); this->readConnection->moveToThread(QCoreApplication::instance()->thread()); QObject::connect(this->readConnection.get(), &Communi::IrcConnection::messageReceived, @@ -31,9 +33,13 @@ AbstractIrcServer::AbstractIrcServer() [this] { this->onConnected(); }); QObject::connect(this->readConnection.get(), &Communi::IrcConnection::disconnected, [this] { this->onDisconnected(); }); + + // listen to reconnect request + this->readConnection->reconnectRequested.connect([this] { this->connect(); }); + // this->writeConnection->reconnectRequested.connect([this] { this->connect(); }); } -Communi::IrcConnection *AbstractIrcServer::getReadConnection() const +IrcConnection *AbstractIrcServer::getReadConnection() const { return this->readConnection.get(); } @@ -231,7 +237,7 @@ void AbstractIrcServer::addFakeMessage(const QString &data) { auto fakeMessage = Communi::IrcMessage::fromData(data.toUtf8(), this->readConnection.get()); - this->privateMessageReceived(qobject_cast(fakeMessage)); + this->messageReceived(fakeMessage); } void AbstractIrcServer::privateMessageReceived(Communi::IrcPrivateMessage *message) diff --git a/src/providers/irc/abstractircserver.hpp b/src/providers/irc/abstractircserver.hpp index ac4b0274..3ac77d88 100644 --- a/src/providers/irc/abstractircserver.hpp +++ b/src/providers/irc/abstractircserver.hpp @@ -2,9 +2,9 @@ #include "channel.hpp" -#include #include #include +#include #include #include @@ -19,7 +19,7 @@ public: virtual ~AbstractIrcServer() = default; // connection - Communi::IrcConnection *getReadConnection() const; + IrcConnection *getReadConnection() const; void connect(); void disconnect(); @@ -33,7 +33,7 @@ public: // signals pajlada::Signals::NoArgSignal connected; pajlada::Signals::NoArgSignal disconnected; - pajlada::Signals::Signal onPrivateMessage; + // pajlada::Signals::Signal onPrivateMessage; void addFakeMessage(const QString &data); @@ -43,8 +43,7 @@ public: protected: AbstractIrcServer(); - virtual void initializeConnection(Communi::IrcConnection *connection, bool isRead, - bool isWrite) = 0; + virtual void initializeConnection(IrcConnection *connection, bool isRead, bool isWrite) = 0; virtual std::shared_ptr createChannel(const QString &channelName) = 0; virtual void privateMessageReceived(Communi::IrcPrivateMessage *message); @@ -64,10 +63,12 @@ protected: private: void initConnection(); - std::unique_ptr writeConnection = nullptr; - std::unique_ptr readConnection = nullptr; + std::unique_ptr writeConnection = nullptr; + std::unique_ptr readConnection = nullptr; std::mutex connectionMutex; + + QTimer pingTimer; }; } // namespace irc diff --git a/src/providers/irc/ircconnection2.cpp b/src/providers/irc/ircconnection2.cpp new file mode 100644 index 00000000..8469bed1 --- /dev/null +++ b/src/providers/irc/ircconnection2.cpp @@ -0,0 +1,36 @@ +#include "ircconnection2.hpp" + +namespace chatterino { +namespace providers { +namespace irc { + +IrcConnection::IrcConnection(QObject *parent) + : Communi::IrcConnection(parent) +{ + this->pingTimer_.setInterval(5000); + this->pingTimer_.start(); + QObject::connect(&this->pingTimer_, &QTimer::timeout, [this] { + if (!this->recentlyReceivedMessage_.load()) { + this->sendRaw("PING"); + this->reconnectTimer_.start(); + } + this->recentlyReceivedMessage_ = false; + }); + + this->reconnectTimer_.setInterval(5000); + this->reconnectTimer_.setSingleShot(true); + QObject::connect(&this->reconnectTimer_, &QTimer::timeout, + [this] { reconnectRequested.invoke(); }); + + QObject::connect(this, &Communi::IrcConnection::messageReceived, [this](Communi::IrcMessage *) { + this->recentlyReceivedMessage_ = true; + + if (this->reconnectTimer_.isActive()) { + this->reconnectTimer_.stop(); + } + }); +} + +} // namespace irc +} // namespace providers +} // namespace chatterino diff --git a/src/providers/irc/ircconnection2.hpp b/src/providers/irc/ircconnection2.hpp new file mode 100644 index 00000000..c9a468b7 --- /dev/null +++ b/src/providers/irc/ircconnection2.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include +#include + +namespace chatterino { +namespace providers { +namespace irc { + +class IrcConnection : public Communi::IrcConnection +{ +public: + IrcConnection(QObject *parent = nullptr); + + pajlada::Signals::NoArgSignal reconnectRequested; + +private: + QTimer pingTimer_; + QTimer reconnectTimer_; + std::atomic recentlyReceivedMessage_{true}; +}; + +} // namespace irc +} // namespace providers +} // namespace chatterino diff --git a/src/providers/twitch/ircmessagehandler.cpp b/src/providers/twitch/ircmessagehandler.cpp index 7111a0f5..09753cac 100644 --- a/src/providers/twitch/ircmessagehandler.cpp +++ b/src/providers/twitch/ircmessagehandler.cpp @@ -1,6 +1,7 @@ #include "ircmessagehandler.hpp" #include "application.hpp" +#include "controllers/highlights/highlightcontroller.hpp" #include "debug/log.hpp" #include "messages/limitedqueue.hpp" #include "messages/message.hpp" @@ -10,6 +11,9 @@ #include "providers/twitch/twitchserver.hpp" #include "singletons/resourcemanager.hpp" #include "singletons/windowmanager.hpp" +#include "util/irchelpers.hpp" + +#include using namespace chatterino::singletons; using namespace chatterino::messages; @@ -24,116 +28,145 @@ IrcMessageHandler &IrcMessageHandler::getInstance() return instance; } +void IrcMessageHandler::handlePrivMessage(Communi::IrcPrivateMessage *message, TwitchServer &server) +{ + this->addMessage(message, message->target(), message->content(), server, false); +} + +void IrcMessageHandler::addMessage(Communi::IrcMessage *message, const QString &target, + const QString &content, TwitchServer &server, bool isSub) +{ + QString channelName; + if (!trimChannelName(target, channelName)) { + return; + } + + auto chan = server.getChannelOrEmpty(channelName); + + if (chan->isEmpty()) { + return; + } + + messages::MessageParseArgs args; + if (isSub) { + args.trimSubscriberUsername = true; + } + + TwitchMessageBuilder builder(chan.get(), message, content, args); + + if (isSub || !builder.isIgnored()) { + messages::MessagePtr msg = builder.build(); + + if (isSub) { + msg->flags |= messages::Message::Subscription; + msg->flags &= ~messages::Message::Highlighted; + } else { + if (msg->flags & messages::Message::Highlighted) { + server.mentionsChannel->addMessage(msg); + getApp()->highlights->addHighlight(msg); + } + } + + chan->addMessage(msg); + } +} + void IrcMessageHandler::handleRoomStateMessage(Communi::IrcMessage *message) { const auto &tags = message->tags(); - auto iterator = tags.find("room-id"); + auto app = getApp(); - if (iterator != tags.end()) { - auto roomID = iterator.value().toString(); + // get twitch channel + QString chanName; + if (!trimChannelName(message->parameter(0), chanName)) { + return; + } + auto chan = app->twitch.server->getChannelOrEmpty(chanName); + TwitchChannel *twitchChannel = dynamic_cast(chan.get()); - QStringList words = QString(message->toData()).split("#"); + if (twitchChannel) { + // room-id + decltype(tags.find("xD")) it; - // ensure the format is valid - if (words.length() < 2) { - return; - } + if ((it = tags.find("room-id")) != tags.end()) { + auto roomID = it.value().toString(); - auto app = getApp(); - - QString channelName = words.at(1); - - auto channel = app->twitch.server->getChannelOrEmpty(channelName); - - if (channel->isEmpty()) { - return; - } - - if (auto twitchChannel = dynamic_cast(channel.get())) { - // set the room id of the channel twitchChannel->setRoomID(roomID); + + app->resources->loadChannelData(roomID); } - app->resources->loadChannelData(roomID); + // Room modes + TwitchChannel::RoomModes roomModes = twitchChannel->getRoomModes(); + + if ((it = tags.find("emote-only")) != tags.end()) { + roomModes.emoteOnly = it.value() == "1"; + } + if ((it = tags.find("subs-only")) != tags.end()) { + roomModes.submode = it.value() == "1"; + } + if ((it = tags.find("slow")) != tags.end()) { + roomModes.slowMode = it.value().toInt(); + } + if ((it = tags.find("r9k")) != tags.end()) { + roomModes.r9k = it.value() == "1"; + } + if ((it = tags.find("broadcaster-lang")) != tags.end()) { + roomModes.broadcasterLang = it.value().toString(); + } + + twitchChannel->setRoomModes(roomModes); } } void IrcMessageHandler::handleClearChatMessage(Communi::IrcMessage *message) { - return; - // // check parameter count - // if (message->parameters().length() < 1) { - // return; - // } + // check parameter count + if (message->parameters().length() < 1) { + return; + } - // QString chanName; - // if (!TrimChannelName(message->parameter(0), chanName)) { - // return; - // } + QString chanName; + if (!trimChannelName(message->parameter(0), chanName)) { + return; + } - // auto app = getApp(); + auto app = getApp(); - // // get channel - // auto chan = app->twitch.server->getChannelOrEmpty(chanName); + // get channel + auto chan = app->twitch.server->getChannelOrEmpty(chanName); - // if (chan->isEmpty()) { - // debug::Log("[IrcMessageHandler:handleClearChatMessage] Twitch channel {} not found", - // chanName); - // return; - // } + if (chan->isEmpty()) { + debug::Log("[IrcMessageHandler:handleClearChatMessage] Twitch channel {} not found", + chanName); + return; + } - // // check if the chat has been cleared by a moderator - // if (message->parameters().length() == 1) { - // chan->addMessage(Message::createSystemMessage("Chat has been cleared by a - // moderator.")); + // check if the chat has been cleared by a moderator + if (message->parameters().length() == 1) { + chan->addMessage(Message::createSystemMessage("Chat has been cleared by a moderator.")); - // return; - // } + return; + } - // // get username, duration and message of the timed out user - // QString username = message->parameter(1); - // QString durationInSeconds, reason; - // QVariant v = message->tag("ban-duration"); - // if (v.isValid()) { - // durationInSeconds = v.toString(); - // } + // get username, duration and message of the timed out user + QString username = message->parameter(1); + QString durationInSeconds, reason; + QVariant v = message->tag("ban-duration"); + if (v.isValid()) { + durationInSeconds = v.toString(); + } - // v = message->tag("ban-reason"); - // if (v.isValid()) { - // reason = v.toString(); - // } + v = message->tag("ban-reason"); + if (v.isValid()) { + reason = v.toString(); + } - // // add the notice that the user has been timed out - // LimitedQueueSnapshot snapshot = chan->getMessageSnapshot(); - // bool addMessage = true; - // int snapshotLength = snapshot.getLength(); + auto timeoutMsg = Message::createTimeoutMessage(username, durationInSeconds, reason, false); + chan->addOrReplaceTimeout(timeoutMsg); - // for (int i = std::max(0, snapshotLength - 20); i < snapshotLength; i++) { - // auto &s = snapshot[i]; - // if (s->flags.HasFlag(Message::Timeout) && s->timeoutUser == username) { - // MessagePtr replacement( - // Message::createTimeoutMessage(username, durationInSeconds, reason, true)); - // chan->replaceMessage(s, replacement); - // addMessage = false; - // break; - // } - // } - - // if (addMessage) { - // chan->addMessage(Message::createTimeoutMessage(username, durationInSeconds, reason, - // false)); - // } - - // // disable the messages from the user - // for (int i = 0; i < snapshotLength; i++) { - // auto &s = snapshot[i]; - // if (!(s->flags & Message::Timeout) && s->loginName == username) { - // s->flags.EnableFlag(Message::Disabled); - // } - // } - - // // refresh all - // app->windows->repaintVisibleChatWidgets(chan.get()); + // refresh all + app->windows->repaintVisibleChatWidgets(chan.get()); } void IrcMessageHandler::handleUserStateMessage(Communi::IrcMessage *message) @@ -144,7 +177,7 @@ void IrcMessageHandler::handleUserStateMessage(Communi::IrcMessage *message) auto app = getApp(); QString channelName; - if (!TrimChannelName(message->parameter(0), channelName)) { + if (!trimChannelName(message->parameter(0), channelName)) { return; } @@ -180,6 +213,8 @@ void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *message) app->twitch.server->mentionsChannel->addMessage(_message); } + app->twitch.server->lastUserThatWhisperedMe.set(builder.userName); + c->addMessage(_message); if (app->settings->inlineWhispers) { @@ -190,9 +225,51 @@ void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *message) } } -void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message) +void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, TwitchServer &server) { - // do nothing + auto data = message->toData(); + + auto tags = message->tags(); + auto parameters = message->parameters(); + + auto target = parameters[0]; + QString msgType = tags.value("msg-id", "").toString(); + QString content; + if (parameters.size() >= 2) { + content = parameters[1]; + } + + if (msgType == "sub" || msgType == "resub" || msgType == "subgift") { + // Sub-specific message. I think it's only allowed for "resub" messages atm + if (!content.isEmpty()) { + this->addMessage(message, target, content, server, true); + } + } + + auto it = tags.find("system-msg"); + + if (it != tags.end()) { + auto newMessage = + messages::Message::createSystemMessage(util::parseTagString(it.value().toString())); + + newMessage->flags |= messages::Message::Subscription; + + QString channelName; + + if (message->parameters().size() < 1) { + return; + } + + if (!trimChannelName(message->parameter(0), channelName)) { + return; + } + + auto chan = server.getChannelOrEmpty(channelName); + + if (!chan->isEmpty()) { + chan->addMessage(newMessage); + } + } } void IrcMessageHandler::handleModeMessage(Communi::IrcMessage *message) @@ -231,7 +308,8 @@ void IrcMessageHandler::handleNoticeMessage(Communi::IrcNoticeMessage *message) // auto channel = app->twitch.server->getChannelOrEmpty(channelName); // if (channel->isEmpty()) { - // debug::Log("[IrcManager:handleNoticeMessage] Channel {} not found in channel manager", + // debug::Log("[IrcManager:handleNoticeMessage] Channel {} not found in channel + // manager", // channelName); // return; // } @@ -257,6 +335,26 @@ void IrcMessageHandler::handleWriteConnectionNoticeMessage(Communi::IrcNoticeMes this->handleNoticeMessage(message); } +void IrcMessageHandler::handleJoinMessage(Communi::IrcMessage *message) +{ + auto app = getApp(); + auto channel = app->twitch.server->getChannelOrEmpty(message->parameter(0).remove(0, 1)); + + if (TwitchChannel *twitchChannel = dynamic_cast(channel.get())) { + twitchChannel->addJoinedUser(message->nick()); + } +} + +void IrcMessageHandler::handlePartMessage(Communi::IrcMessage *message) +{ + auto app = getApp(); + auto channel = app->twitch.server->getChannelOrEmpty(message->parameter(0).remove(0, 1)); + + if (TwitchChannel *twitchChannel = dynamic_cast(channel.get())) { + twitchChannel->addPartedUser(message->nick()); + } +} + } // namespace twitch } // namespace providers } // namespace chatterino diff --git a/src/providers/twitch/ircmessagehandler.hpp b/src/providers/twitch/ircmessagehandler.hpp index d79296a9..dca4c16b 100644 --- a/src/providers/twitch/ircmessagehandler.hpp +++ b/src/providers/twitch/ircmessagehandler.hpp @@ -6,6 +6,8 @@ namespace chatterino { namespace providers { namespace twitch { +class TwitchServer; + class IrcMessageHandler { IrcMessageHandler() = default; @@ -13,14 +15,23 @@ class IrcMessageHandler public: static IrcMessageHandler &getInstance(); + void handlePrivMessage(Communi::IrcPrivateMessage *message, TwitchServer &server); + void handleRoomStateMessage(Communi::IrcMessage *message); void handleClearChatMessage(Communi::IrcMessage *message); void handleUserStateMessage(Communi::IrcMessage *message); void handleWhisperMessage(Communi::IrcMessage *message); - void handleUserNoticeMessage(Communi::IrcMessage *message); + void handleUserNoticeMessage(Communi::IrcMessage *message, TwitchServer &server); void handleModeMessage(Communi::IrcMessage *message); void handleNoticeMessage(Communi::IrcNoticeMessage *message); void handleWriteConnectionNoticeMessage(Communi::IrcNoticeMessage *message); + + void handleJoinMessage(Communi::IrcMessage *message); + void handlePartMessage(Communi::IrcMessage *message); + +private: + void addMessage(Communi::IrcMessage *message, const QString &target, const QString &content, + TwitchServer &server, bool isResub); }; } // namespace twitch diff --git a/src/providers/twitch/pubsub.cpp b/src/providers/twitch/pubsub.cpp index e8af28b0..a0a4fc96 100644 --- a/src/providers/twitch/pubsub.cpp +++ b/src/providers/twitch/pubsub.cpp @@ -3,7 +3,6 @@ #include "debug/log.hpp" #include "providers/twitch/pubsubactions.hpp" #include "providers/twitch/pubsubhelpers.hpp" -#include "singletons/accountmanager.hpp" #include "util/rapidjson-helpers.hpp" #include diff --git a/src/providers/twitch/pubsubhelpers.cpp b/src/providers/twitch/pubsubhelpers.cpp index f27ac94d..395ae44f 100644 --- a/src/providers/twitch/pubsubhelpers.cpp +++ b/src/providers/twitch/pubsubhelpers.cpp @@ -1,7 +1,6 @@ #include "providers/twitch/pubsubhelpers.hpp" #include "providers/twitch/pubsubactions.hpp" -#include "singletons/accountmanager.hpp" #include "util/rapidjson-helpers.hpp" namespace chatterino { diff --git a/src/providers/twitch/twitchaccount.cpp b/src/providers/twitch/twitchaccount.cpp index c089a7c2..4e893fe2 100644 --- a/src/providers/twitch/twitchaccount.cpp +++ b/src/providers/twitch/twitchaccount.cpp @@ -12,7 +12,7 @@ namespace twitch { TwitchAccount::TwitchAccount(const QString &_username, const QString &_oauthToken, const QString &_oauthClient, const QString &_userID) - : controllers::accounts::Account("Twitch") + : controllers::accounts::Account(ProviderId::Twitch) , oauthClient(_oauthClient) , oauthToken(_oauthToken) , userName(_username) diff --git a/src/providers/twitch/twitchaccount.hpp b/src/providers/twitch/twitchaccount.hpp index 649fd1f8..86f14153 100644 --- a/src/providers/twitch/twitchaccount.hpp +++ b/src/providers/twitch/twitchaccount.hpp @@ -55,7 +55,6 @@ public: bool isAnon() const; void loadIgnores(); - void ignore(const QString &targetName, std::function onFinished); void ignoreByID(const QString &targetUserID, const QString &targetName, diff --git a/src/providers/twitch/twitchaccountmanager.cpp b/src/providers/twitch/twitchaccountmanager.cpp index dcfd9bb4..1b8b24e8 100644 --- a/src/providers/twitch/twitchaccountmanager.cpp +++ b/src/providers/twitch/twitchaccountmanager.cpp @@ -15,6 +15,9 @@ TwitchAccountManager::TwitchAccountManager() auto currentUser = this->getCurrent(); currentUser->loadIgnores(); }); + + this->accounts.itemRemoved.connect( + [this](const auto &acc) { this->removeUser(acc.item.get()); }); } std::shared_ptr TwitchAccountManager::getCurrent() @@ -32,7 +35,7 @@ std::vector TwitchAccountManager::getUsernames() const std::lock_guard lock(this->mutex); - for (const auto &user : this->users) { + for (const auto &user : this->accounts.getVector()) { userNames.push_back(user->getUserName()); } @@ -44,7 +47,7 @@ std::shared_ptr TwitchAccountManager::findUserByUsername( { std::lock_guard lock(this->mutex); - for (const auto &user : this->users) { + for (const auto &user : this->accounts.getVector()) { if (username.compare(user->getUserName(), Qt::CaseInsensitive) == 0) { return user; } @@ -134,25 +137,16 @@ void TwitchAccountManager::load() }); } -bool TwitchAccountManager::removeUser(const QString &username) +bool TwitchAccountManager::removeUser(TwitchAccount *account) { - if (!this->userExists(username)) { - return false; + const auto &accs = this->accounts.getVector(); + + std::string userID(account->getUserId().toStdString()); + if (!userID.empty()) { + pajlada::Settings::SettingManager::removeSetting("/accounts/uid" + userID); } - this->mutex.lock(); - this->users.erase(std::remove_if(this->users.begin(), this->users.end(), [username](auto user) { - if (user->getUserName() == username) { - std::string userID(user->getUserId().toStdString()); - assert(!userID.empty()); - pajlada::Settings::SettingManager::removeSetting("/accounts/uid" + userID); - return true; - } - return false; - })); - this->mutex.unlock(); - - if (username == qS(this->currentUsername.getValue())) { + if (account->getUserName() == qS(this->currentUsername.getValue())) { // The user that was removed is the current user, log into the anonymous user this->currentUsername = ""; } @@ -187,9 +181,9 @@ TwitchAccountManager::AddUserResponse TwitchAccountManager::addUser( auto newUser = std::make_shared(userData.username, userData.oauthToken, userData.clientID, userData.userID); - std::lock_guard lock(this->mutex); + // std::lock_guard lock(this->mutex); - this->users.push_back(newUser); + this->accounts.insertItem(newUser); return AddUserResponse::UserAdded; } diff --git a/src/providers/twitch/twitchaccountmanager.hpp b/src/providers/twitch/twitchaccountmanager.hpp index 2486ad8c..b7fe99f0 100644 --- a/src/providers/twitch/twitchaccountmanager.hpp +++ b/src/providers/twitch/twitchaccountmanager.hpp @@ -15,10 +15,11 @@ // namespace chatterino { - -namespace singletons { -class AccountManager; -} // namespace singletons +namespace controllers { +namespace accounts { +class AccountController; +} +} // namespace controllers namespace providers { namespace twitch { @@ -46,8 +47,6 @@ public: void reloadUsers(); void load(); - bool removeUser(const QString &username); - pajlada::Settings::Setting currentUsername = {"/accounts/current", ""}; pajlada::Signals::NoArgSignal currentUserChanged; pajlada::Signals::NoArgSignal userListUpdated; @@ -63,14 +62,14 @@ private: UserAdded, }; AddUserResponse addUser(const UserData &data); + bool removeUser(TwitchAccount *account); std::shared_ptr currentUser; std::shared_ptr anonymousUser; - std::vector> users; mutable std::mutex mutex; - friend class chatterino::singletons::AccountManager; + friend class chatterino::controllers::accounts::AccountController; }; } // namespace twitch diff --git a/src/providers/twitch/twitchchannel.cpp b/src/providers/twitch/twitchchannel.cpp index 4acff566..369945b4 100644 --- a/src/providers/twitch/twitchchannel.cpp +++ b/src/providers/twitch/twitchchannel.cpp @@ -5,7 +5,6 @@ #include "messages/message.hpp" #include "providers/twitch/pubsub.hpp" #include "providers/twitch/twitchmessagebuilder.hpp" -#include "singletons/accountmanager.hpp" #include "singletons/emotemanager.hpp" #include "singletons/ircmanager.hpp" #include "singletons/settingsmanager.hpp" @@ -45,7 +44,8 @@ TwitchChannel::TwitchChannel(const QString &channelName, Communi::IrcConnection this->refreshLiveStatus(); // }); - this->managedConnect(app->accounts->Twitch.currentUserChanged, [this]() { this->setMod(false); }); + this->managedConnect(app->accounts->twitch.currentUserChanged, + [this]() { this->setMod(false); }); auto refreshPubSubState = [=]() { if (!this->hasModRights()) { @@ -56,7 +56,7 @@ TwitchChannel::TwitchChannel(const QString &channelName, Communi::IrcConnection return; } - auto account = app->accounts->Twitch.getCurrent(); + auto account = app->accounts->twitch.getCurrent(); if (account && !account->getUserId().isEmpty()) { app->twitch.pubsub->listenToChannelModerationActions(this->roomID, account); } @@ -64,7 +64,7 @@ TwitchChannel::TwitchChannel(const QString &channelName, Communi::IrcConnection this->userStateChanged.connect(refreshPubSubState); this->roomIDchanged.connect(refreshPubSubState); - this->managedConnect(app->accounts->Twitch.currentUserChanged, refreshPubSubState); + this->managedConnect(app->accounts->twitch.currentUserChanged, refreshPubSubState); refreshPubSubState(); this->fetchMessages.connect([this] { @@ -85,7 +85,7 @@ TwitchChannel::TwitchChannel(const QString &channelName, Communi::IrcConnection }; auto doRefreshChatters = [=]() { - const auto streamStatus = this->GetStreamStatus(); + const auto streamStatus = this->getStreamStatus(); if (app->settings->onlyFetchChattersForSmallerStreamers) { if (streamStatus.live && streamStatus.viewerCount > app->settings->smallStreamerLimit) { @@ -102,6 +102,10 @@ TwitchChannel::TwitchChannel(const QString &channelName, Communi::IrcConnection this->chattersListTimer = new QTimer; QObject::connect(this->chattersListTimer, &QTimer::timeout, doRefreshChatters); this->chattersListTimer->start(5 * 60 * 1000); + + // for (int i = 0; i < 1000; i++) { + // this->addMessage(messages::Message::createSystemMessage("asdf")); + // } } TwitchChannel::~TwitchChannel() @@ -186,7 +190,7 @@ bool TwitchChannel::isBroadcaster() { auto app = getApp(); - return this->name == app->accounts->Twitch.getCurrent()->getUserName(); + return this->name == app->accounts->twitch.getCurrent()->getUserName(); } bool TwitchChannel::hasModRights() @@ -206,6 +210,93 @@ void TwitchChannel::addRecentChatter(const std::shared_ptr &m this->completionModel.addUser(message->displayName); } +void TwitchChannel::addJoinedUser(const QString &user) +{ + auto *app = getApp(); + if (user == app->accounts->twitch.getCurrent()->getUserName() || + !app->settings->showJoins.getValue()) { + return; + } + + std::lock_guard guard(this->joinedUserMutex); + + joinedUsers << user; + + if (!this->joinedUsersMergeQueued) { + this->joinedUsersMergeQueued = true; + + QTimer::singleShot(500, &this->object, [this] { + std::lock_guard guard(this->joinedUserMutex); + + auto message = messages::Message::createSystemMessage("Users joined: " + + this->joinedUsers.join(", ")); + message->flags |= messages::Message::Collapsed; + this->addMessage(message); + this->joinedUsers.clear(); + this->joinedUsersMergeQueued = false; + }); + } +} + +void TwitchChannel::addPartedUser(const QString &user) +{ + auto *app = getApp(); + + if (user == app->accounts->twitch.getCurrent()->getUserName() || + !app->settings->showJoins.getValue()) { + return; + } + + std::lock_guard guard(this->partedUserMutex); + + partedUsers << user; + + if (!this->partedUsersMergeQueued) { + this->partedUsersMergeQueued = true; + + QTimer::singleShot(500, &this->object, [this] { + std::lock_guard guard(this->partedUserMutex); + + auto message = messages::Message::createSystemMessage("Users parted: " + + this->partedUsers.join(", ")); + message->flags |= messages::Message::Collapsed; + this->addMessage(message); + this->partedUsers.clear(); + + this->partedUsersMergeQueued = false; + }); + } +} + +TwitchChannel::RoomModes TwitchChannel::getRoomModes() +{ + std::lock_guard lock(this->roomModeMutex); + + return this->roomModes; +} + +void TwitchChannel::setRoomModes(const RoomModes &_roomModes) +{ + { + std::lock_guard lock(this->roomModeMutex); + this->roomModes = _roomModes; + } + + this->roomModesChanged.invoke(); +} + +bool TwitchChannel::isLive() const +{ + std::lock_guard lock(this->streamStatusMutex); + return this->streamStatus.live; +} + +TwitchChannel::StreamStatus TwitchChannel::getStreamStatus() const +{ + std::lock_guard lock(this->streamStatusMutex); + return this->streamStatus; +} + void TwitchChannel::setLive(bool newLiveStatus) { bool gotNewLiveStatus = false; @@ -289,6 +380,11 @@ void TwitchChannel::refreshLiveStatus() QString::number(diff / 3600) + "h " + QString::number(diff % 3600 / 60) + "m"; channel->streamStatus.rerun = false; + if (stream.HasMember("stream_type")) { + channel->streamStatus.streamType = stream["stream_type"].GetString(); + } else { + channel->streamStatus.streamType = QString(); + } if (stream.HasMember("broadcast_platform")) { const auto &broadcastPlatformValue = stream["broadcast_platform"]; diff --git a/src/providers/twitch/twitchchannel.hpp b/src/providers/twitch/twitchchannel.hpp index 4e5531d7..d1637b85 100644 --- a/src/providers/twitch/twitchchannel.hpp +++ b/src/providers/twitch/twitchchannel.hpp @@ -7,6 +7,7 @@ #include "singletons/emotemanager.hpp" #include "singletons/ircmanager.hpp" #include "util/concurrentmap.hpp" +#include "util/mutexvalue.hpp" #include @@ -31,6 +32,7 @@ public: QString title; QString game; QString uptime; + QString streamType; }; struct UserState { @@ -38,6 +40,15 @@ public: bool broadcaster; }; + struct RoomModes { + bool submode = false; + bool r9k = false; + bool emoteOnly = false; + // int folowerOnly = 0; + int slowMode = 0; + QString broadcasterLang; + }; + ~TwitchChannel() final; void reloadChannelEmotes(); @@ -52,6 +63,8 @@ public: bool hasModRights(); void addRecentChatter(const std::shared_ptr &message) final; + void addJoinedUser(const QString &user); + void addPartedUser(const QString &user); const std::shared_ptr bttvChannelEmotes; const std::shared_ptr ffzChannelEmotes; @@ -66,25 +79,21 @@ public: pajlada::Signals::NoArgBoltSignal fetchMessages; pajlada::Signals::NoArgSignal userStateChanged; + pajlada::Signals::NoArgSignal roomModesChanged; QString roomID; - StreamStatus GetStreamStatus() const - { - std::lock_guard lock(this->streamStatusMutex); - return this->streamStatus; - } + RoomModes getRoomModes(); + void setRoomModes(const RoomModes &roomModes); + + StreamStatus getStreamStatus() const; struct NameOptions { QString displayName; QString localizedName; }; - bool IsLive() const - { - std::lock_guard lock(this->streamStatusMutex); - return this->streamStatus.live; - } + bool isLive() const; private: explicit TwitchChannel(const QString &channelName, Communi::IrcConnection *readConnection); @@ -103,6 +112,16 @@ private: bool mod; QByteArray messageSuffix; QString lastSentMessage; + RoomModes roomModes; + std::mutex roomModeMutex; + + QObject object; + std::mutex joinedUserMutex; + QStringList joinedUsers; + bool joinedUsersMergeQueued = false; + std::mutex partedUserMutex; + QStringList partedUsers; + bool partedUsersMergeQueued = false; Communi::IrcConnection *readConnection; diff --git a/src/providers/twitch/twitchhelpers.cpp b/src/providers/twitch/twitchhelpers.cpp index 4f6f663a..98140c6c 100644 --- a/src/providers/twitch/twitchhelpers.cpp +++ b/src/providers/twitch/twitchhelpers.cpp @@ -5,7 +5,7 @@ namespace chatterino { namespace providers { namespace twitch { -bool TrimChannelName(const QString &channelName, QString &outChannelName) +bool trimChannelName(const QString &channelName, QString &outChannelName) { if (channelName.length() < 3) { debug::Log("channel name length below 3"); diff --git a/src/providers/twitch/twitchhelpers.hpp b/src/providers/twitch/twitchhelpers.hpp index 997821d6..56f0bb94 100644 --- a/src/providers/twitch/twitchhelpers.hpp +++ b/src/providers/twitch/twitchhelpers.hpp @@ -6,7 +6,7 @@ namespace chatterino { namespace providers { namespace twitch { -bool TrimChannelName(const QString &channelName, QString &outChannelName); +bool trimChannelName(const QString &channelName, QString &outChannelName); } // namespace twitch } // namespace providers diff --git a/src/providers/twitch/twitchmessagebuilder.cpp b/src/providers/twitch/twitchmessagebuilder.cpp index 172cc325..98631704 100644 --- a/src/providers/twitch/twitchmessagebuilder.cpp +++ b/src/providers/twitch/twitchmessagebuilder.cpp @@ -1,11 +1,11 @@ #include "providers/twitch/twitchmessagebuilder.hpp" #include "application.hpp" +#include "controllers/accounts/accountcontroller.hpp" #include "controllers/highlights/highlightcontroller.hpp" #include "controllers/ignores/ignorecontroller.hpp" #include "debug/log.hpp" #include "providers/twitch/twitchchannel.hpp" -#include "singletons/accountmanager.hpp" #include "singletons/emotemanager.hpp" #include "singletons/ircmanager.hpp" #include "singletons/resourcemanager.hpp" @@ -68,7 +68,18 @@ bool TwitchMessageBuilder::isIgnored() const if (app->settings->enableTwitchIgnoredUsers && this->tags.contains("user-id")) { auto sourceUserID = this->tags.value("user-id").toString(); - for (const auto &user : app->accounts->Twitch.getCurrent()->getIgnores()) { + for (const auto &user : app->accounts->twitch.getCurrent()->getIgnores()) { + if (sourceUserID == user.id) { + debug::Log("Blocking message because it's from blocked user {}", user.name); + return true; + } + } + } + + if (app->settings->enableTwitchIgnoredUsers && this->tags.contains("user-id")) { + auto sourceUserID = this->tags.value("user-id").toString(); + + for (const auto &user : app->accounts->twitch.getCurrent()->getIgnores()) { if (sourceUserID == user.id) { debug::Log("Blocking message because it's from blocked user {}", user.name); return true; @@ -86,12 +97,14 @@ MessagePtr TwitchMessageBuilder::build() // PARSING this->parseUsername(); -#ifdef XD - if (this->originalMessage.length() > 100) { - this->message->flags |= Message::Collapsed; - this->emplace(getApp()->resources->badgeCollapsed, MessageElement::Collapsed); - } -#endif + //#ifdef XD + // if (this->originalMessage.length() > 100) { + // this->message->flags |= Message::Collapsed; + // this->emplace(getApp()->resources->badgeCollapsed, + // MessageElement::Collapsed); + // } + //#endif + this->message->flags |= Message::Collapsed; // PARSING this->parseMessageID(); @@ -141,15 +154,8 @@ MessagePtr TwitchMessageBuilder::build() this->appendTwitchEmote(ircMessage, emote, twitchEmotes); } - struct { - bool operator()(const std::pair &lhs, - const std::pair &rhs) - { - return lhs.first < rhs.first; - } - } customLess; - - std::sort(twitchEmotes.begin(), twitchEmotes.end(), customLess); + std::sort(twitchEmotes.begin(), twitchEmotes.end(), + [](const auto &a, const auto &b) { return a.first < b.first; }); } auto currentTwitchEmote = twitchEmotes.begin(); @@ -282,10 +288,16 @@ void TwitchMessageBuilder::parseUsername() // username this->userName = this->ircMessage->nick(); - if (this->userName.isEmpty()) { + if (this->userName.isEmpty() || this->args.trimSubscriberUsername) { this->userName = this->tags.value(QLatin1String("login")).toString(); } + // display name + // auto displayNameVariant = this->tags.value("display-name"); + // if (displayNameVariant.isValid()) { + // this->userName = displayNameVariant.toString() + " (" + this->userName + ")"; + // } + this->message->loginName = this->userName; } @@ -350,14 +362,14 @@ void TwitchMessageBuilder::appendUsername() } else if (this->args.isReceivedWhisper) { // Sender username this->emplace(usernameText, MessageElement::Text, this->usernameColor, - FontStyle::MediumBold) + FontStyle::ChatMediumBold) ->setLink({Link::UserInfo, this->userName}); - auto currentUser = app->accounts->Twitch.getCurrent(); + auto currentUser = app->accounts->twitch.getCurrent(); // Separator this->emplace("->", MessageElement::Text, - app->themes->messages.textColors.system, FontStyle::Medium); + app->themes->messages.textColors.system, FontStyle::ChatMedium); QColor selfColor = currentUser->color; if (!selfColor.isValid()) { @@ -366,14 +378,14 @@ void TwitchMessageBuilder::appendUsername() // Your own username this->emplace(currentUser->getUserName() + ":", MessageElement::Text, - selfColor, FontStyle::MediumBold); + selfColor, FontStyle::ChatMediumBold); } else { if (!this->action) { usernameText += ":"; } this->emplace(usernameText, MessageElement::Text, this->usernameColor, - FontStyle::MediumBold) + FontStyle::ChatMediumBold) ->setLink({Link::UserInfo, this->userName}); } } @@ -385,7 +397,7 @@ void TwitchMessageBuilder::parseHighlights() auto app = getApp(); - auto currentUser = app->accounts->Twitch.getCurrent(); + auto currentUser = app->accounts->twitch.getCurrent(); QString currentUsername = currentUser->getUserName(); @@ -571,11 +583,13 @@ void TwitchMessageBuilder::appendTwitchBadges() QString cheerAmountQS = badge.mid(5); std::string versionKey = cheerAmountQS.toStdString(); + QString tooltip = QString("Twitch cheer ") + cheerAmountQS; // Try to fetch channel-specific bit badge try { const auto &badge = channelResources.badgeSets.at("bits").versions.at(versionKey); - this->emplace(badge.badgeImage1x, MessageElement::BadgeVanity); + this->emplace(badge.badgeImage1x, MessageElement::BadgeVanity) + ->setTooltip(tooltip); continue; } catch (const std::out_of_range &) { // Channel does not contain a special bit badge for this version @@ -584,7 +598,8 @@ void TwitchMessageBuilder::appendTwitchBadges() // Use default bit badge try { const auto &badge = app->resources->badgeSets.at("bits").versions.at(versionKey); - this->emplace(badge.badgeImage1x, MessageElement::BadgeVanity); + this->emplace(badge.badgeImage1x, MessageElement::BadgeVanity) + ->setTooltip(tooltip); } catch (const std::out_of_range &) { debug::Log("No default bit badge for version {} found", versionKey); continue; diff --git a/src/providers/twitch/twitchserver.cpp b/src/providers/twitch/twitchserver.cpp index 6a6843f5..bcb028fd 100644 --- a/src/providers/twitch/twitchserver.cpp +++ b/src/providers/twitch/twitchserver.cpp @@ -1,16 +1,18 @@ #include "twitchserver.hpp" #include "application.hpp" +#include "controllers/accounts/accountcontroller.hpp" +#include "controllers/highlights/highlightcontroller.hpp" #include "providers/twitch/ircmessagehandler.hpp" #include "providers/twitch/twitchaccount.hpp" #include "providers/twitch/twitchhelpers.hpp" #include "providers/twitch/twitchmessagebuilder.hpp" -#include "singletons/accountmanager.hpp" #include "util/posttothread.hpp" +#include #include -using namespace Communi; +// using namespace Communi; using namespace chatterino::singletons; namespace chatterino { @@ -27,13 +29,14 @@ TwitchServer::TwitchServer() void TwitchServer::initialize() { - getApp()->accounts->Twitch.currentUserChanged.connect( + getApp()->accounts->twitch.currentUserChanged.connect( [this]() { util::postToThread([this] { this->connect(); }); }); } -void TwitchServer::initializeConnection(IrcConnection *connection, bool isRead, bool isWrite) +void TwitchServer::initializeConnection(providers::irc::IrcConnection *connection, bool isRead, + bool isWrite) { - std::shared_ptr account = getApp()->accounts->Twitch.getCurrent(); + std::shared_ptr account = getApp()->accounts->twitch.getCurrent(); qDebug() << "logging in as" << account->getUserName(); @@ -56,9 +59,9 @@ void TwitchServer::initializeConnection(IrcConnection *connection, bool isRead, // this->refreshIgnoredUsers(username, oauthClient, oauthToken); } - connection->sendCommand(IrcCommand::createCapability("REQ", "twitch.tv/membership")); - connection->sendCommand(IrcCommand::createCapability("REQ", "twitch.tv/commands")); - connection->sendCommand(IrcCommand::createCapability("REQ", "twitch.tv/tags")); + connection->sendCommand(Communi::IrcCommand::createCapability("REQ", "twitch.tv/membership")); + connection->sendCommand(Communi::IrcCommand::createCapability("REQ", "twitch.tv/commands")); + connection->sendCommand(Communi::IrcCommand::createCapability("REQ", "twitch.tv/tags")); connection->setHost("irc.chat.twitch.tv"); connection->setPort(6667); @@ -74,69 +77,53 @@ std::shared_ptr TwitchServer::createChannel(const QString &channelName) return std::shared_ptr(channel); } -void TwitchServer::privateMessageReceived(IrcPrivateMessage *message) +void TwitchServer::privateMessageReceived(Communi::IrcPrivateMessage *message) { - QString channelName; - if (!TrimChannelName(message->target(), channelName)) { - return; - } - - this->onPrivateMessage.invoke(message); - auto chan = this->getChannelOrEmpty(channelName); - - if (chan->isEmpty()) { - return; - } - - messages::MessageParseArgs args; - - TwitchMessageBuilder builder(chan.get(), message, args); - - if (!builder.isIgnored()) { - messages::MessagePtr _message = builder.build(); - if (_message->flags & messages::Message::Highlighted) { - this->mentionsChannel->addMessage(_message); - } - - chan->addMessage(_message); - } + IrcMessageHandler::getInstance().handlePrivMessage(message, *this); } -void TwitchServer::messageReceived(IrcMessage *message) +void TwitchServer::messageReceived(Communi::IrcMessage *message) { // this->readConnection - if (message->type() == IrcMessage::Type::Private) { + if (message->type() == Communi::IrcMessage::Type::Private) { // We already have a handler for private messages return; } const QString &command = message->command(); + auto &handler = IrcMessageHandler::getInstance(); + if (command == "ROOMSTATE") { - IrcMessageHandler::getInstance().handleRoomStateMessage(message); + handler.handleRoomStateMessage(message); } else if (command == "CLEARCHAT") { - IrcMessageHandler::getInstance().handleClearChatMessage(message); + handler.handleClearChatMessage(message); } else if (command == "USERSTATE") { - IrcMessageHandler::getInstance().handleUserStateMessage(message); + handler.handleUserStateMessage(message); } else if (command == "WHISPER") { - IrcMessageHandler::getInstance().handleWhisperMessage(message); + handler.handleWhisperMessage(message); } else if (command == "USERNOTICE") { - IrcMessageHandler::getInstance().handleUserNoticeMessage(message); + handler.handleUserNoticeMessage(message, *this); } else if (command == "MODE") { - IrcMessageHandler::getInstance().handleModeMessage(message); + handler.handleModeMessage(message); } else if (command == "NOTICE") { - IrcMessageHandler::getInstance().handleNoticeMessage( - static_cast(message)); + handler.handleNoticeMessage(static_cast(message)); + } else if (command == "JOIN") { + handler.handleJoinMessage(message); + } else if (command == "PART") { + handler.handlePartMessage(message); } } -void TwitchServer::writeConnectionMessageReceived(IrcMessage *message) +void TwitchServer::writeConnectionMessageReceived(Communi::IrcMessage *message) { switch (message->type()) { - case IrcMessage::Type::Notice: { + case Communi::IrcMessage::Type::Notice: { IrcMessageHandler::getInstance().handleWriteConnectionNoticeMessage( - static_cast(message)); + static_cast(message)); } break; + + default:; } } diff --git a/src/providers/twitch/twitchserver.hpp b/src/providers/twitch/twitchserver.hpp index 99852197..caa93fe2 100644 --- a/src/providers/twitch/twitchserver.hpp +++ b/src/providers/twitch/twitchserver.hpp @@ -3,6 +3,7 @@ #include "providers/irc/abstractircserver.hpp" #include "providers/twitch/twitchaccount.hpp" #include "providers/twitch/twitchchannel.hpp" +#include "util/mutexvalue.hpp" #include @@ -22,12 +23,17 @@ public: std::shared_ptr getChannelOrEmptyByID(const QString &channelID); + util::MutexValue lastUserThatWhisperedMe; + + // QString getLastWhisperedPerson() const; + // void setLastWhisperedPerson(const QString &person); + const ChannelPtr whispersChannel; const ChannelPtr mentionsChannel; IndirectChannel watchingChannel; protected: - void initializeConnection(Communi::IrcConnection *connection, bool isRead, + void initializeConnection(providers::irc::IrcConnection *connection, bool isRead, bool isWrite) override; std::shared_ptr createChannel(const QString &channelName) override; @@ -38,6 +44,10 @@ protected: std::shared_ptr getCustomChannel(const QString &channelname) override; QString cleanChannelName(const QString &dirtyChannelName) override; + +private: + // mutable std::mutex lastWhisperedPersonMutex; + // QString lastWhisperedPerson; }; } // namespace twitch diff --git a/src/singletons/accountmanager.cpp b/src/singletons/accountmanager.cpp deleted file mode 100644 index 65dc2a08..00000000 --- a/src/singletons/accountmanager.cpp +++ /dev/null @@ -1,12 +0,0 @@ -#include "singletons/accountmanager.hpp" - -namespace chatterino { -namespace singletons { - -void AccountManager::load() -{ - this->Twitch.load(); -} - -} // namespace singletons -} // namespace chatterino diff --git a/src/singletons/accountmanager.hpp b/src/singletons/accountmanager.hpp deleted file mode 100644 index de7a38ab..00000000 --- a/src/singletons/accountmanager.hpp +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include "providers/twitch/twitchaccountmanager.hpp" - -namespace chatterino { -namespace singletons { - -class AccountManager -{ -public: - AccountManager() = default; - - ~AccountManager() = delete; - - void load(); - - providers::twitch::TwitchAccountManager Twitch; -}; - -} // namespace singletons -} // namespace chatterino diff --git a/src/singletons/emotemanager.cpp b/src/singletons/emotemanager.cpp index 3cd841f0..aeddc38b 100644 --- a/src/singletons/emotemanager.cpp +++ b/src/singletons/emotemanager.cpp @@ -86,8 +86,8 @@ EmoteManager::EmoteManager() void EmoteManager::initialize() { - getApp()->accounts->Twitch.currentUserChanged.connect([this] { - auto currentUser = getApp()->accounts->Twitch.getCurrent(); + getApp()->accounts->twitch.currentUserChanged.connect([this] { + auto currentUser = getApp()->accounts->twitch.getCurrent(); assert(currentUser); this->refreshTwitchEmotes(currentUser); }); @@ -130,13 +130,23 @@ void EmoteManager::reloadBTTVChannelEmotes(const QString &channelName, QString code = emoteObject.value("code").toString(); // emoteObject.value("imageType").toString(); - QString link = linkTemplate; - link.detach(); + auto emote = this->getBTTVChannelEmoteFromCaches().getOrAdd(id, [&] { + util::EmoteData emoteData; + QString link = linkTemplate; + link.detach(); + emoteData.image1x = new Image(link.replace("{{id}}", id).replace("{{image}}", "1x"), + 1, code, code + "
Channel BTTV Emote"); + link = linkTemplate; + link.detach(); + emoteData.image2x = new Image(link.replace("{{id}}", id).replace("{{image}}", "2x"), + 0.5, code, code + "
Channel BTTV Emote"); + link = linkTemplate; + link.detach(); + emoteData.image3x = new Image(link.replace("{{id}}", id).replace("{{image}}", "3x"), + 0.25, code, code + "
Channel BTTV Emote"); + emoteData.pageLink = "https://manage.betterttv.net/emotes/" + id; - link = link.replace("{{id}}", id).replace("{{image}}", "1x"); - - auto emote = this->getBTTVChannelEmoteFromCaches().getOrAdd(id, [&code, &link] { - return util::EmoteData(new Image(link, 1, code, code + "
Channel BTTV Emote")); + return emoteData; }); this->bttvChannelEmotes.insert(code, emote); @@ -182,9 +192,11 @@ void EmoteManager::reloadFFZChannelEmotes(const QString &channelName, QJsonObject urls = emoteObject.value("urls").toObject(); - auto emote = this->getFFZChannelEmoteFromCaches().getOrAdd(id, [&code, &urls] { + auto emote = this->getFFZChannelEmoteFromCaches().getOrAdd(id, [id, &code, &urls] { util::EmoteData emoteData; FillInFFZEmoteData(urls, code, code + "
Channel FFZ Emote", emoteData); + emoteData.pageLink = + QString("https://www.frankerfacez.com/emoticon/%1-%2").arg(id).arg(code); return emoteData; }); @@ -436,6 +448,7 @@ void EmoteManager::refreshTwitchEmotes(const std::shared_ptr &use [=, &emoteData](const QJsonObject &root) { emoteData.emoteSets.clear(); emoteData.emoteCodes.clear(); + auto emoticonSets = root.value("emoticon_sets").toObject(); for (QJsonObject::iterator it = emoticonSets.begin(); it != emoticonSets.end(); ++it) { std::string emoteSetString = it.key().toStdString(); @@ -443,7 +456,7 @@ void EmoteManager::refreshTwitchEmotes(const std::shared_ptr &use for (QJsonValue emoteValue : emoteSetList) { QJsonObject emoticon = emoteValue.toObject(); - std::string id = emoticon["id"].toString().toStdString(); + std::string id = QString::number(emoticon["id"].toInt()).toStdString(); std::string code = emoticon["code"].toString().toStdString(); emoteData.emoteSets[emoteSetString].push_back({id, code}); emoteData.emoteCodes.push_back(code); @@ -483,6 +496,7 @@ void EmoteManager::loadBTTVEmotes() code + "
Global BTTV Emote"); emoteData.image3x = new Image(GetBTTVEmoteLink(urlTemplate, id, "3x"), 0.25, code, code + "
Global BTTV Emote"); + emoteData.pageLink = "https://manage.betterttv.net/emotes/" + id; this->bttvGlobalEmotes.insert(code, emoteData); codes.push_back(code.toStdString()); @@ -510,10 +524,13 @@ void EmoteManager::loadFFZEmotes() QJsonObject object = emote.toObject(); QString code = object.value("name").toString(); + int id = object.value("id").toInt(); QJsonObject urls = object.value("urls").toObject(); util::EmoteData emoteData; FillInFFZEmoteData(urls, code, code + "
Global FFZ Emote", emoteData); + emoteData.pageLink = + QString("https://www.frankerfacez.com/emoticon/%1-%2").arg(id).arg(code); this->ffzGlobalEmotes.insert(code, emoteData); codes.push_back(code.toStdString()); @@ -530,6 +547,20 @@ util::EmoteData EmoteManager::getTwitchEmoteById(long id, const QString &emoteNa { QString _emoteName = emoteName; _emoteName.replace("<", "<"); + _emoteName.replace(">", ">"); + + static QMap emoteNameReplacements{ + {"[oO](_|\\.)[oO]", "O_o"}, {"\\>\\;\\(", ">("}, {"\\<\\;3", "<3"}, + {"\\:-?(o|O)", ":O"}, {"\\:-?(p|P)", ":P"}, {"\\:-?[\\\\/]", ":/"}, + {"\\:-?[z|Z|\\|]", ":Z"}, {"\\:-?\\(", ":("}, {"\\:-?\\)", ":)"}, + {"\\:-?D", ":D"}, {"\\;-?(p|P)", ";P"}, {"\\;-?\\)", ";)"}, + {"R-?\\)", "R)"}, {"B-?\\)", "B)"}, + }; + + auto it = emoteNameReplacements.find(_emoteName); + if (it != emoteNameReplacements.end()) { + _emoteName = it.value(); + } return _twitchEmoteFromCache.getOrAdd(id, [&emoteName, &_emoteName, &id] { util::EmoteData newEmoteData; diff --git a/src/singletons/emotemanager.hpp b/src/singletons/emotemanager.hpp index 483c16d1..37b56ca6 100644 --- a/src/singletons/emotemanager.hpp +++ b/src/singletons/emotemanager.hpp @@ -46,16 +46,6 @@ public: util::EmoteData getTwitchEmoteById(long int id, const QString &emoteName); - int getGeneration() - { - return _generation; - } - - void incGeneration() - { - _generation++; - } - pajlada::Signals::NoArgSignal &getGifUpdateSignal(); // Bit badge/emotes? @@ -145,8 +135,6 @@ private: pajlada::Signals::NoArgSignal gifUpdateTimerSignal; QTimer gifUpdateTimer; bool gifUpdateTimerInitiated = false; - - int _generation = 0; }; } // namespace singletons diff --git a/src/singletons/fontmanager.cpp b/src/singletons/fontmanager.cpp index 59a5e874..24049248 100644 --- a/src/singletons/fontmanager.cpp +++ b/src/singletons/fontmanager.cpp @@ -3,6 +3,10 @@ #include #include +#include "application.hpp" +#include "util/assertinguithread.hpp" +#include "windowmanager.hpp" + #ifdef Q_OS_WIN32 #define DEFAULT_FONT_FAMILY "Segoe UI" #define DEFAULT_FONT_SIZE 10 @@ -20,86 +24,111 @@ namespace chatterino { namespace singletons { FontManager::FontManager() - : currentFontFamily("/appearance/currentFontFamily", DEFAULT_FONT_FAMILY) - , currentFontSize("/appearance/currentFontSize", DEFAULT_FONT_SIZE) -// , currentFont(this->currentFontFamily.getValue().c_str(), currentFontSize.getValue()) + : chatFontFamily("/appearance/currentFontFamily", DEFAULT_FONT_FAMILY) + , chatFontSize("/appearance/currentFontSize", DEFAULT_FONT_SIZE) { qDebug() << "init FontManager"; - this->currentFontFamily.connect([this](const std::string &newValue, auto) { - this->incGeneration(); - // this->currentFont.setFamily(newValue.c_str()); - this->currentFontByScale.clear(); - this->fontChanged.invoke(); - }); + this->chatFontFamily.connect([this](const std::string &, auto) { + util::assertInGuiThread(); - this->currentFontSize.connect([this](const int &newValue, auto) { - this->incGeneration(); - // this->currentFont.setSize(newValue); - this->currentFontByScale.clear(); - this->fontChanged.invoke(); - }); -} - -QFont &FontManager::getFont(FontManager::Type type, float scale) -{ - // return this->currentFont.getFont(type); - return this->getCurrentFont(scale).getFont(type); -} - -QFontMetrics &FontManager::getFontMetrics(FontManager::Type type, float scale) -{ - // return this->currentFont.getFontMetrics(type); - return this->getCurrentFont(scale).getFontMetrics(type); -} - -FontManager::FontData &FontManager::Font::getFontData(FontManager::Type type) -{ - switch (type) { - case Tiny: - return this->tiny; - case Small: - return this->small; - case MediumSmall: - return this->mediumSmall; - case Medium: - return this->medium; - case MediumBold: - return this->mediumBold; - case MediumItalic: - return this->mediumItalic; - case Large: - return this->large; - case VeryLarge: - return this->veryLarge; - default: - qDebug() << "Unknown font type:" << type << ", defaulting to medium"; - return this->medium; - } -} - -QFont &FontManager::Font::getFont(Type type) -{ - return this->getFontData(type).font; -} - -QFontMetrics &FontManager::Font::getFontMetrics(Type type) -{ - return this->getFontData(type).metrics; -} - -FontManager::Font &FontManager::getCurrentFont(float scale) -{ - for (auto it = this->currentFontByScale.begin(); it != this->currentFontByScale.end(); it++) { - if (it->first == scale) { - return it->second; + if (getApp()->windows) { + getApp()->windows->incGeneration(); } - } - this->currentFontByScale.push_back( - std::make_pair(scale, Font(this->currentFontFamily.getValue().c_str(), - this->currentFontSize.getValue() * scale))); - return this->currentFontByScale.back().second; + for (auto &map : this->fontsByType) { + map.clear(); + } + this->fontChanged.invoke(); + }); + + this->chatFontSize.connect([this](const int &, auto) { + util::assertInGuiThread(); + + if (getApp()->windows) { + getApp()->windows->incGeneration(); + } + + for (auto &map : this->fontsByType) { + map.clear(); + } + this->fontChanged.invoke(); + }); + + this->fontsByType.resize(size_t(EndType)); +} + +QFont FontManager::getFont(FontManager::Type type, float scale) +{ + return this->getOrCreateFontData(type, scale).font; +} + +QFontMetrics FontManager::getFontMetrics(FontManager::Type type, float scale) +{ + return this->getOrCreateFontData(type, scale).metrics; +} + +FontManager::FontData &FontManager::getOrCreateFontData(Type type, float scale) +{ + util::assertInGuiThread(); + + assert(type >= 0 && type < EndType); + + auto &map = this->fontsByType[size_t(type)]; + + // find element + auto it = map.find(scale); + if (it != map.end()) { + // return if found + + return it->second; + } + + // emplace new element + auto result = map.emplace(scale, this->createFontData(type, scale)); + assert(result.second); + + return result.first->second; +} + +FontManager::FontData FontManager::createFontData(Type type, float scale) +{ + // check if it's a chat (scale the setting) + if (type >= ChatStart && type <= ChatEnd) { + static std::unordered_map sizeScale{ + {ChatSmall, {0.6f, false, QFont::Normal}}, + {ChatMediumSmall, {0.8f, false, QFont::Normal}}, + {ChatMedium, {1, false, QFont::Normal}}, + {ChatMediumBold, {1, false, QFont::Medium}}, + {ChatMediumItalic, {1, true, QFont::Normal}}, + {ChatLarge, {1.2f, false, QFont::Normal}}, + {ChatVeryLarge, {1.4f, false, QFont::Normal}}, + }; + + auto data = sizeScale[type]; + return FontData(QFont(QString::fromStdString(this->chatFontFamily.getValue()), + int(this->chatFontSize.getValue() * data.scale * scale), data.weight, + data.italic)); + } + + // normal Ui font (use pt size) + { +#ifdef Q_OS_MAC + constexpr float multiplier = 0.8f; +#else + constexpr float multiplier = 1.f; +#endif + + static std::unordered_map defaultSize{ + {Tiny, {8, "Monospace", false, QFont::Normal}}, + {UiMedium, {int(12 * multiplier), DEFAULT_FONT_FAMILY, false, QFont::Normal}}, + {UiTabs, {int(9 * multiplier), DEFAULT_FONT_FAMILY, false, QFont::Normal}}, + }; + + UiFontData &data = defaultSize[type]; + QFont font(data.name, int(data.size * scale), data.weight, data.italic); + return FontData(font); + } } } // namespace singletons diff --git a/src/singletons/fontmanager.hpp b/src/singletons/fontmanager.hpp index 8f7d4eaa..4e4f489e 100644 --- a/src/singletons/fontmanager.hpp +++ b/src/singletons/fontmanager.hpp @@ -3,138 +3,79 @@ #include #include #include +#include +#include #include #include +#include namespace chatterino { namespace singletons { -class FontManager +class FontManager : boost::noncopyable { public: FontManager(); - FontManager(const FontManager &) = delete; - FontManager(FontManager &&) = delete; - ~FontManager() = delete; - + // font data gets set in createFontData(...) enum Type : uint8_t { Tiny, - Small, - MediumSmall, - Medium, - MediumBold, - MediumItalic, - Large, - VeryLarge, + ChatSmall, + ChatMediumSmall, + ChatMedium, + ChatMediumBold, + ChatMediumItalic, + ChatLarge, + ChatVeryLarge, + + UiMedium, + UiTabs, + + // don't remove this value + EndType, + + // make sure to update these values accordingly! + ChatStart = ChatSmall, + ChatEnd = ChatVeryLarge, }; - QFont &getFont(Type type, float scale); - QFontMetrics &getFontMetrics(Type type, float scale); + QFont getFont(Type type, float scale); + QFontMetrics getFontMetrics(Type type, float scale); - int getGeneration() const - { - return this->generation; - } - - void incGeneration() - { - this->generation++; - } - - pajlada::Settings::Setting currentFontFamily; - pajlada::Settings::Setting currentFontSize; + pajlada::Settings::Setting chatFontFamily; + pajlada::Settings::Setting chatFontSize; pajlada::Signals::NoArgSignal fontChanged; private: struct FontData { - FontData(QFont &&_font) + FontData(const QFont &_font) : font(_font) - , metrics(this->font) + , metrics(_font) { } - QFont font; - QFontMetrics metrics; + const QFont font; + const QFontMetrics metrics; }; - struct Font { - Font() = delete; - - Font(const char *fontFamilyName, int mediumSize) - : tiny(QFont("Monospace", 8)) - , small(QFont(fontFamilyName, mediumSize - 4)) - , mediumSmall(QFont(fontFamilyName, mediumSize - 2)) - , medium(QFont(fontFamilyName, mediumSize)) - , mediumBold(QFont(fontFamilyName, mediumSize, QFont::DemiBold)) - , mediumItalic(QFont(fontFamilyName, mediumSize, -1, true)) - , large(QFont(fontFamilyName, mediumSize)) - , veryLarge(QFont(fontFamilyName, mediumSize)) - { - tiny.font.setStyleHint(QFont::TypeWriter); - } - - void setFamily(const char *newFamily) - { - this->small.font.setFamily(newFamily); - this->mediumSmall.font.setFamily(newFamily); - this->medium.font.setFamily(newFamily); - this->mediumBold.font.setFamily(newFamily); - this->mediumItalic.font.setFamily(newFamily); - this->large.font.setFamily(newFamily); - this->veryLarge.font.setFamily(newFamily); - - this->updateMetrics(); - } - - void setSize(int newMediumSize) - { - this->small.font.setPointSize(newMediumSize - 4); - this->mediumSmall.font.setPointSize(newMediumSize - 2); - this->medium.font.setPointSize(newMediumSize); - this->mediumBold.font.setPointSize(newMediumSize); - this->mediumItalic.font.setPointSize(newMediumSize); - this->large.font.setPointSize(newMediumSize + 2); - this->veryLarge.font.setPointSize(newMediumSize + 4); - - this->updateMetrics(); - } - - void updateMetrics() - { - this->small.metrics = QFontMetrics(this->small.font); - this->mediumSmall.metrics = QFontMetrics(this->mediumSmall.font); - this->medium.metrics = QFontMetrics(this->medium.font); - this->mediumBold.metrics = QFontMetrics(this->mediumBold.font); - this->mediumItalic.metrics = QFontMetrics(this->mediumItalic.font); - this->large.metrics = QFontMetrics(this->large.font); - this->veryLarge.metrics = QFontMetrics(this->veryLarge.font); - } - - FontData &getFontData(Type type); - - QFont &getFont(Type type); - QFontMetrics &getFontMetrics(Type type); - - FontData tiny; - FontData small; - FontData mediumSmall; - FontData medium; - FontData mediumBold; - FontData mediumItalic; - FontData large; - FontData veryLarge; + struct ChatFontData { + float scale; + bool italic; + QFont::Weight weight; }; - Font &getCurrentFont(float scale); + struct UiFontData { + float size; + const char *name; + bool italic; + QFont::Weight weight; + }; - // Future plans: - // Could have multiple fonts in here, such as "Menu font", "Application font", "Chat font" + FontData &getOrCreateFontData(Type type, float scale); + FontData createFontData(Type type, float scale); - std::list> currentFontByScale; - - int generation = 0; + std::vector> fontsByType; }; } // namespace singletons diff --git a/src/singletons/nativemessagingmanager.cpp b/src/singletons/nativemessagingmanager.cpp index fcccec23..64578e75 100644 --- a/src/singletons/nativemessagingmanager.cpp +++ b/src/singletons/nativemessagingmanager.cpp @@ -31,6 +31,7 @@ namespace ipc = boost::interprocess; namespace chatterino { namespace singletons { +// fourtf: don't add this class to the application class NativeMessagingManager::NativeMessagingManager() { qDebug() << "init NativeMessagingManager"; @@ -50,6 +51,10 @@ void NativeMessagingManager::registerHost() { auto app = getApp(); + if (app->paths->isPortable()) { + return; + } + // create manifest QJsonDocument document; QJsonObject root_obj; @@ -113,13 +118,14 @@ void NativeMessagingManager::ReceiverThread::run() while (true) { try { - char *buf = (char *)malloc(MESSAGE_SIZE); + std::unique_ptr buf(static_cast(malloc(MESSAGE_SIZE))); ipc::message_queue::size_type retSize; unsigned int priority; - messageQueue.receive(buf, MESSAGE_SIZE, retSize, priority); + messageQueue.receive(buf.get(), MESSAGE_SIZE, retSize, priority); - QJsonDocument document = QJsonDocument::fromJson(QByteArray(buf, retSize)); + QJsonDocument document = + QJsonDocument::fromJson(QByteArray::fromRawData(buf.get(), retSize)); this->handleMessage(document.object()); } catch (ipc::interprocess_exception &ex) { @@ -143,26 +149,37 @@ void NativeMessagingManager::ReceiverThread::handleMessage(const QJsonObject &ro QString _type = root.value("type").toString(); bool attach = root.value("attach").toBool(); QString name = root.value("name").toString(); - QString winId = root.value("winId").toString(); - int yOffset = root.value("yOffset").toInt(-1); - if (_type.isNull() || name.isNull() || winId.isNull()) { +#ifdef USEWINSDK + widgets::AttachedWindow::GetArgs args; + args.winId = root.value("winId").toString(); + args.yOffset = root.value("yOffset").toInt(-1); + args.width = root.value("size").toObject().value("width").toInt(-1); + args.height = root.value("size").toObject().value("height").toInt(-1); + + if (_type.isNull() || args.winId.isNull()) { qDebug() << "NM type, name or winId missing"; attach = false; return; } +#endif if (_type == "twitch") { - util::postToThread([name, attach, winId, yOffset, app] { - app->twitch.server->watchingChannel.update( - app->twitch.server->getOrAddChannel(name)); + util::postToThread([=] { + if (!name.isEmpty()) { + app->twitch.server->watchingChannel.update( + app->twitch.server->getOrAddChannel(name)); + } if (attach) { #ifdef USEWINSDK - auto *window = - widgets::AttachedWindow::get(::GetForegroundWindow(), winId, yOffset); - window->setChannel(app->twitch.server->getOrAddChannel(name)); - window->show(); + if (args.height != -1) { + auto *window = widgets::AttachedWindow::get(::GetForegroundWindow(), args); + if (!name.isEmpty()) { + window->setChannel(app->twitch.server->getOrAddChannel(name)); + } + } +// window->show(); #endif } }); @@ -179,12 +196,15 @@ void NativeMessagingManager::ReceiverThread::handleMessage(const QJsonObject &ro } #ifdef USEWINSDK - util::postToThread([winId] { widgets::AttachedWindow::detach(winId); }); + util::postToThread([winId] { + qDebug() << "NW detach"; + widgets::AttachedWindow::detach(winId); + }); #endif } else { qDebug() << "NM unknown action " + action; } -} +} // namespace singletons } // namespace singletons } // namespace chatterino diff --git a/src/singletons/nativemessagingmanager.hpp b/src/singletons/nativemessagingmanager.hpp index b4d17ab2..36e8a78e 100644 --- a/src/singletons/nativemessagingmanager.hpp +++ b/src/singletons/nativemessagingmanager.hpp @@ -8,6 +8,7 @@ namespace singletons { class NativeMessagingManager { public: + // fourtf: don't add this class to the application class NativeMessagingManager(); ~NativeMessagingManager() = delete; diff --git a/src/singletons/pathmanager.cpp b/src/singletons/pathmanager.cpp index 07f5a43b..fb87e6a4 100644 --- a/src/singletons/pathmanager.cpp +++ b/src/singletons/pathmanager.cpp @@ -19,21 +19,21 @@ PathManager::PathManager(int argc, char **argv) .replace("/", "x"); // Options - bool portable = false; + this->portable = false; for (int i = 1; i < argc; ++i) { if (strcmp(argv[i], "portable") == 0) { - portable = true; + this->portable = true; } } - if (QFileInfo::exists(QCoreApplication::applicationDirPath() + "/portable")) { - portable = true; + if (QFileInfo::exists(QCoreApplication::applicationDirPath() + "/this->portable")) { + this->portable = true; } // Root path = %APPDATA%/Chatterino or the folder that the executable resides in QString rootPath; - if (portable) { + if (this->portable) { rootPath.append(QCoreApplication::applicationDirPath()); } else { // Get settings path @@ -91,5 +91,10 @@ bool PathManager::createFolder(const QString &folderPath) return QDir().mkpath(folderPath); } +bool PathManager::isPortable() +{ + return this->portable; +} + } // namespace singletons } // namespace chatterino diff --git a/src/singletons/pathmanager.hpp b/src/singletons/pathmanager.hpp index 1c4dd888..5d377587 100644 --- a/src/singletons/pathmanager.hpp +++ b/src/singletons/pathmanager.hpp @@ -28,6 +28,10 @@ public: QString appPathHash; bool createFolder(const QString &folderPath); + bool isPortable(); + +private: + bool portable; }; } // namespace singletons diff --git a/src/singletons/resourcemanager.hpp b/src/singletons/resourcemanager.hpp index 2da50658..487b0f41 100644 --- a/src/singletons/resourcemanager.hpp +++ b/src/singletons/resourcemanager.hpp @@ -2,6 +2,7 @@ #include "util/emotemap.hpp" +#include #include #include diff --git a/src/singletons/settingsmanager.cpp b/src/singletons/settingsmanager.cpp index 2516df09..27bca154 100644 --- a/src/singletons/settingsmanager.cpp +++ b/src/singletons/settingsmanager.cpp @@ -39,8 +39,18 @@ void SettingManager::initialize() this->timestampFormat.connect([](auto, auto) { auto app = getApp(); - app->windows->layoutVisibleChatWidgets(); + app->windows->layoutChannelViews(); }); + + this->emoteScale.connect([](auto, auto) { getApp()->windows->forceLayoutChannelViews(); }); + + this->timestampFormat.connect([](auto, auto) { getApp()->windows->forceLayoutChannelViews(); }); + this->alternateMessageBackground.connect( + [](auto, auto) { getApp()->windows->forceLayoutChannelViews(); }); + this->seperateMessages.connect( + [](auto, auto) { getApp()->windows->forceLayoutChannelViews(); }); + this->collpseMessagesMinLines.connect( + [](auto, auto) { getApp()->windows->forceLayoutChannelViews(); }); } MessageElement::Flags SettingManager::getWordFlags() diff --git a/src/singletons/settingsmanager.hpp b/src/singletons/settingsmanager.hpp index acf65d74..c3f275d2 100644 --- a/src/singletons/settingsmanager.hpp +++ b/src/singletons/settingsmanager.hpp @@ -40,6 +40,8 @@ public: BoolSetting hideEmptyInput = {"/appearance/hideEmptyInputBox", false}; BoolSetting showMessageLength = {"/appearance/messages/showMessageLength", false}; BoolSetting seperateMessages = {"/appearance/messages/separateMessages", false}; + // BoolSetting collapseLongMessages = {"/appearance/messages/collapseLongMessages", false}; + IntSetting collpseMessagesMinLines = {"/appearance/messages/collapseMessagesMinLines", 0}; BoolSetting alternateMessageBackground = {"/appearance/messages/alternateMessageBackground", false}; BoolSetting windowTopMost = {"/appearance/windowAlwaysOnTop", false}; @@ -54,6 +56,8 @@ public: /// Behaviour BoolSetting allowDuplicateMessages = {"/behaviour/allowDuplicateMessages", true}; BoolSetting mentionUsersWithAt = {"/behaviour/mentionUsersWithAt", false}; + BoolSetting showJoins = {"/behaviour/showJoins", false}; + BoolSetting showParts = {"/behaviour/showParts", false}; FloatSetting mouseScrollMultiplier = {"/behaviour/mouseScrollMultiplier", 1.0}; // Auto-completion @@ -75,10 +79,10 @@ public: BoolSetting enableGifAnimations = {"/emotes/enableGifAnimations", true}; FloatSetting emoteScale = {"/emotes/scale", 1.f}; - // 0 = Smallest size - // 1 = One size above 0 (usually size of 0 * 2) - // 2 = One size above 1 (usually size of 1 * 2) - // etc... + // 0 = No preference + // 1 = 1x + // 2 = 2x + // 3 = 3x IntSetting preferredEmoteQuality = {"/emotes/preferredEmoteQuality", 0}; /// Links diff --git a/src/singletons/thememanager.cpp b/src/singletons/thememanager.cpp index 8296dbef..5c626603 100644 --- a/src/singletons/thememanager.cpp +++ b/src/singletons/thememanager.cpp @@ -52,7 +52,6 @@ void ThemeManager::actuallyUpdate(double hue, double multiplier) isLight = multiplier > 0; bool lightWin = isLight; - QColor none(0, 0, 0, 0); QColor themeColor = QColor::fromHslF(hue, 0.43, 0.5); QColor themeColorNoSat = QColor::fromHslF(hue, 0, 0.5); @@ -69,7 +68,7 @@ void ThemeManager::actuallyUpdate(double hue, double multiplier) #ifdef Q_OS_LINUX this->window.background = lightWin ? "#fff" : QColor(61, 60, 56); #else - this->window.background = lightWin ? "#fff" : "#444"; + this->window.background = lightWin ? "#fff" : "#111"; #endif QColor fg = this->window.text = lightWin ? "#000" : "#eee"; @@ -89,27 +88,52 @@ void ThemeManager::actuallyUpdate(double hue, double multiplier) /// TABS if (lightWin) { - this->tabs.regular = {fg, {bg, QColor("#ccc"), bg}}; + this->tabs.regular = {QColor("#444"), + {QColor("#fff"), QColor("#fff"), QColor("#fff")}, + {QColor("#fff"), QColor("#fff"), QColor("#fff")}}; 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.highlighted = {fg, {QColor("#ccc"), QColor("#ccc"), QColor("#bbb")}}; - this->tabs.selected = {QColor("#fff"), - {QColor("#777"), QColor("#777"), QColor("#888")}}; - } else { - this->tabs.regular = {fg, {bg, QColor("#555"), bg}}; - 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")}}; + fg, {bg, QColor("#ccc"), bg}, {QColor("#aaa"), QColor("#aaa"), QColor("#aaa")}}; + this->tabs.highlighted = {fg, + {bg, QColor("#ccc"), bg}, + {QColor("#b60505"), QColor("#b60505"), QColor("#b60505")}}; this->tabs.selected = {QColor("#000"), - {QColor("#999"), QColor("#999"), QColor("#888")}}; + {QColor("#b4d7ff"), QColor("#b4d7ff"), QColor("#b4d7ff")}, + {QColor("#00aeef"), QColor("#00aeef"), QColor("#00aeef")}}; + } else { + this->tabs.regular = {QColor("#aaa"), + {QColor("#252525"), QColor("#252525"), QColor("#252525")}, + {QColor("#444"), QColor("#444"), QColor("#444")}}; + this->tabs.newMessage = {fg, + {QColor("#252525"), QColor("#252525"), QColor("#252525")}, + {QColor("#888"), QColor("#888"), QColor("#888")}}; + this->tabs.highlighted = {fg, + {QColor("#252525"), QColor("#252525"), QColor("#252525")}, + {QColor("#ee6166"), QColor("#ee6166"), QColor("#ee6166")}}; + + this->tabs.selected = {QColor("#fff"), + {QColor("#555555"), QColor("#555555"), QColor("#555555")}, + {QColor("#00aeef"), QColor("#00aeef"), QColor("#00aeef")}}; } + // scrollbar + 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.bottomLine = this->tabs.selected.backgrounds.regular.color(); } @@ -119,7 +143,12 @@ void ThemeManager::actuallyUpdate(double hue, double multiplier) this->splits.messageSeperator = isLight ? QColor(127, 127, 127) : QColor(60, 60, 60); this->splits.background = getColor(0, sat, 1); this->splits.dropPreview = QColor(0, 148, 255, 0x30); - this->splits.dropPreviewBorder = QColor(0, 148, 255, 0x70); + this->splits.dropPreviewBorder = QColor(0, 148, 255, 0xff); + this->splits.dropTargetRect = QColor(0, 148, 255, 0x00); + 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.border // this->splits.borderFocused @@ -143,7 +172,10 @@ void ThemeManager::actuallyUpdate(double hue, double multiplier) this->messages.backgrounds.regular = splits.background; this->messages.backgrounds.alternate = getColor(0, sat, 0.93); this->messages.backgrounds.highlighted = - blendColors(themeColor, this->messages.backgrounds.regular, 0.8); + blendColors(themeColor, this->messages.backgrounds.regular, 0.6); + this->messages.backgrounds.subscription = + blendColors(QColor("#C466FF"), this->messages.backgrounds.regular, 0.7); + // this->messages.backgrounds.resub // this->messages.backgrounds.whisper this->messages.disabled = getColor(0, sat, 1, 0.6); @@ -151,7 +183,9 @@ void ThemeManager::actuallyUpdate(double hue, double multiplier) // this->messages.seperatorInner = // Scrollbar - this->scrollbars.background = getColor(0, sat, 0.94); + this->scrollbars.background = QColor(0, 0, 0, 0); + // this->scrollbars.background = splits.background; + // this->scrollbars.background.setAlphaF(qreal(0.2)); this->scrollbars.thumb = getColor(0, sat, 0.80); this->scrollbars.thumbSelected = getColor(0, sat, 0.7); @@ -163,13 +197,13 @@ void ThemeManager::actuallyUpdate(double hue, double multiplier) this->messages.selection = isLightTheme() ? QColor(0, 0, 0, 64) : QColor(255, 255, 255, 64); this->updated.invoke(); -} +} // namespace singletons QColor ThemeManager::blendColors(const QColor &color1, const QColor &color2, qreal ratio) { - int r = color1.red() * (1 - ratio) + color2.red() * ratio; - int g = color1.green() * (1 - ratio) + color2.green() * ratio; - int b = color1.blue() * (1 - ratio) + color2.blue() * 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); } @@ -177,22 +211,22 @@ QColor ThemeManager::blendColors(const QColor &color1, const QColor &color2, qre void ThemeManager::normalizeColor(QColor &color) { if (this->isLight) { - if (color.lightnessF() > 0.5f) { - color.setHslF(color.hueF(), color.saturationF(), 0.5f); + if (color.lightnessF() > 0.5) { + color.setHslF(color.hueF(), color.saturationF(), 0.5); } - if (color.lightnessF() > 0.4f && color.hueF() > 0.1 && color.hueF() < 0.33333) { + if (color.lightnessF() > 0.4 && color.hueF() > 0.1 && color.hueF() < 0.33333) { color.setHslF( color.hueF(), color.saturationF(), color.lightnessF() - sin((color.hueF() - 0.1) / (0.3333 - 0.1) * 3.14159) * color.saturationF() * 0.2); } } else { - if (color.lightnessF() < 0.5f) { - color.setHslF(color.hueF(), color.saturationF(), 0.5f); + if (color.lightnessF() < 0.5) { + color.setHslF(color.hueF(), color.saturationF(), 0.5); } - if (color.lightnessF() < 0.6f && color.hueF() > 0.54444 && color.hueF() < 0.83333) { + if (color.lightnessF() < 0.6 && color.hueF() > 0.54444 && color.hueF() < 0.83333) { color.setHslF( color.hueF(), color.saturationF(), color.lightnessF() + sin((color.hueF() - 0.54444) / (0.8333 - 0.54444) * 3.14159) * diff --git a/src/singletons/thememanager.hpp b/src/singletons/thememanager.hpp index 443f0482..63254130 100644 --- a/src/singletons/thememanager.hpp +++ b/src/singletons/thememanager.hpp @@ -25,11 +25,16 @@ public: struct TabColors { QColor text; - struct Backgrounds { + struct { QBrush regular; QBrush hover; QBrush unfocused; } backgrounds; + struct { + QColor regular; + QColor hover; + QColor unfocused; + } line; }; /// WINDOW @@ -43,9 +48,9 @@ public: /// TABS struct { TabColors regular; - TabColors selected; - TabColors highlighted; TabColors newMessage; + TabColors highlighted; + TabColors selected; QColor border; QColor bottomLine; } tabs; @@ -58,6 +63,10 @@ public: QColor borderFocused; QColor dropPreview; QColor dropPreviewBorder; + QColor dropTargetRect; + QColor dropTargetRectBorder; + QColor resizeHandle; + QColor resizeHandleBackground; struct { QColor border; @@ -89,7 +98,7 @@ public: QColor regular; QColor alternate; QColor highlighted; - // QColor resub; + QColor subscription; // QColor whisper; } backgrounds; @@ -104,8 +113,10 @@ public: QColor background; QColor thumb; QColor thumbSelected; - // const int highlightsCount = 3; - // QColor highlights[3]; + struct { + QColor highlight; + QColor subscription; + } highlights; } scrollbars; /// TOOLTIP diff --git a/src/singletons/updatemanager.cpp b/src/singletons/updatemanager.cpp index 73b99717..ad270db7 100644 --- a/src/singletons/updatemanager.cpp +++ b/src/singletons/updatemanager.cpp @@ -7,25 +7,31 @@ namespace chatterino { namespace singletons { UpdateManager::UpdateManager() - : currentVersion(CHATTERINO_VERSION) + : currentVersion_(CHATTERINO_VERSION) { qDebug() << "init UpdateManager"; } UpdateManager &UpdateManager::getInstance() { + // fourtf: don't add this class to the application class static UpdateManager instance; + return instance; } const QString &UpdateManager::getCurrentVersion() const { - return this->getCurrentVersion(); + return currentVersion_; } const QString &UpdateManager::getOnlineVersion() const { - return this->getOnlineVersion(); + return onlineVersion_; +} + +void UpdateManager::installUpdates() +{ } void UpdateManager::checkForUpdates() @@ -33,17 +39,38 @@ void UpdateManager::checkForUpdates() QString url = "https://notitia.chatterino.com/version/chatterino/" CHATTERINO_OS "/stable"; util::NetworkRequest req(url); - req.setTimeout(20000); + req.setTimeout(30000); req.getJSON([this](QJsonObject &object) { QJsonValue version_val = object.value("version"); if (!version_val.isString()) { + this->setStatus_(Error); qDebug() << "error updating"; return; } - this->onlineVersion = version_val.toString(); - this->onlineVersionUpdated.invoke(); + this->onlineVersion_ = version_val.toString(); + + if (this->currentVersion_ != this->onlineVersion_) { + this->setStatus_(UpdateAvailable); + } else { + this->setStatus_(NoUpdateAvailable); + } }); + this->setStatus_(Searching); + req.execute(); +} + +UpdateManager::UpdateStatus UpdateManager::getStatus() const +{ + return this->status_; +} + +void UpdateManager::setStatus_(UpdateStatus status) +{ + if (this->status_ != status) { + this->status_ = status; + this->statusUpdated.invoke(status); + } } } // namespace singletons diff --git a/src/singletons/updatemanager.hpp b/src/singletons/updatemanager.hpp index f0e6ff65..365bc21f 100644 --- a/src/singletons/updatemanager.hpp +++ b/src/singletons/updatemanager.hpp @@ -11,17 +11,25 @@ class UpdateManager UpdateManager(); public: + enum UpdateStatus { None, Searching, UpdateAvailable, NoUpdateAvailable, Error }; + + // fourtf: don't add this class to the application class static UpdateManager &getInstance(); void checkForUpdates(); const QString &getCurrentVersion() const; const QString &getOnlineVersion() const; + void installUpdates(); + UpdateStatus getStatus() const; - pajlada::Signals::NoArgSignal onlineVersionUpdated; + pajlada::Signals::Signal statusUpdated; private: - QString currentVersion; - QString onlineVersion; + QString currentVersion_; + QString onlineVersion_; + UpdateStatus status_ = None; + + void setStatus_(UpdateStatus status); }; } // namespace singletons diff --git a/src/singletons/windowmanager.cpp b/src/singletons/windowmanager.cpp index fa949937..69e0046b 100644 --- a/src/singletons/windowmanager.cpp +++ b/src/singletons/windowmanager.cpp @@ -10,6 +10,7 @@ #include "widgets/accountswitchpopupwidget.hpp" #include "widgets/settingsdialog.hpp" +#include #include #include @@ -20,6 +21,9 @@ namespace chatterino { namespace singletons { +using SplitNode = widgets::SplitContainer::Node; +using SplitDirection = widgets::SplitContainer::Direction; + void WindowManager::showSettingsDialog() { QTimer::singleShot(80, [] { widgets::SettingsDialog::showDialog(); }); @@ -54,11 +58,17 @@ WindowManager::WindowManager() qDebug() << "init WindowManager"; } -void WindowManager::layoutVisibleChatWidgets(Channel *channel) +void WindowManager::layoutChannelViews(Channel *channel) { this->layout.invoke(channel); } +void WindowManager::forceLayoutChannelViews() +{ + this->incGeneration(); + this->layoutChannelViews(nullptr); +} + void WindowManager::repaintVisibleChatWidgets(Channel *channel) { if (this->mainWindow != nullptr) { @@ -184,32 +194,40 @@ void WindowManager::initialize() // load tabs QJsonArray tabs = window_obj.value("tabs").toArray(); for (QJsonValue tab_val : tabs) { - widgets::SplitContainer *tab = window.getNotebook().addNewPage(); + widgets::SplitContainer *page = window.getNotebook().addPage(false); QJsonObject tab_obj = tab_val.toObject(); // set custom title QJsonValue title_val = tab_obj.value("title"); if (title_val.isString()) { - tab->getTab()->setTitle(title_val.toString()); - tab->getTab()->useDefaultTitle = false; + page->getTab()->setCustomTitle(title_val.toString()); } // selected if (tab_obj.value("selected").toBool(false)) { - window.getNotebook().select(tab); + window.getNotebook().select(page); } // load splits + QJsonObject splitRoot = tab_obj.value("splits2").toObject(); + + if (!splitRoot.isEmpty()) { + page->decodeFromJson(splitRoot); + + continue; + } + + // fallback load splits (old) int colNr = 0; for (QJsonValue column_val : tab_obj.value("splits").toArray()) { for (QJsonValue split_val : column_val.toArray()) { - widgets::Split *split = new widgets::Split(tab); + widgets::Split *split = new widgets::Split(page); QJsonObject split_obj = split_val.toObject(); - split->setChannel(this->decodeChannel(split_obj)); + split->setChannel(decodeChannel(split_obj)); - tab->addToLayout(split, std::make_pair(colNr, 10000000)); + page->appendSplit(split); } colNr++; } @@ -218,7 +236,7 @@ void WindowManager::initialize() if (mainWindow == nullptr) { mainWindow = &createWindow(widgets::Window::Main); - mainWindow->getNotebook().addNewPage(true); + mainWindow->getNotebook().addPage(true); } this->initialized = true; @@ -255,13 +273,15 @@ void WindowManager::save() // window tabs QJsonArray tabs_arr; - for (int tab_i = 0; tab_i < window->getNotebook().tabCount(); tab_i++) { + for (int tab_i = 0; tab_i < window->getNotebook().getPageCount(); tab_i++) { QJsonObject tab_obj; - widgets::SplitContainer *tab = window->getNotebook().tabAt(tab_i); + widgets::SplitContainer *tab = + dynamic_cast(window->getNotebook().getPageAt(tab_i)); + assert(tab != nullptr); // custom tab title - if (!tab->getTab()->useDefaultTitle) { - tab_obj.insert("title", tab->getTab()->getTitle()); + if (tab->getTab()->hasCustomTitle()) { + tab_obj.insert("title", tab->getTab()->getCustomTitle()); } // selected @@ -270,23 +290,11 @@ void WindowManager::save() } // splits - QJsonArray columns_arr; - std::vector> columns = tab->getColumns(); + QJsonObject splits; - for (std::vector &cells : columns) { - QJsonArray cells_arr; + this->encodeNodeRecusively(tab->getBaseNode(), splits); - for (widgets::Split *cell : cells) { - QJsonObject cell_obj; - - this->encodeChannel(cell->getIndirectChannel(), cell_obj); - - cells_arr.append(cell_obj); - } - columns_arr.append(cells_arr); - } - - tab_obj.insert("splits", columns_arr); + tab_obj.insert("splits2", splits); tabs_arr.append(tab_obj); } @@ -302,10 +310,46 @@ void WindowManager::save() QString settingsPath = app->paths->settingsFolderPath + SETTINGS_FILENAME; QFile file(settingsPath); file.open(QIODevice::WriteOnly | QIODevice::Truncate); - file.write(document.toJson()); + + QJsonDocument::JsonFormat format = +#ifdef _DEBUG + QJsonDocument::JsonFormat::Compact +#else + (QJsonDocument::JsonFormat)0 +#endif + ; + + file.write(document.toJson(format)); file.flush(); } +void WindowManager::encodeNodeRecusively(SplitNode *node, QJsonObject &obj) +{ + switch (node->getType()) { + case SplitNode::_Split: { + obj.insert("type", "split"); + QJsonObject split; + encodeChannel(node->getSplit()->getIndirectChannel(), split); + obj.insert("data", split); + obj.insert("flexh", node->getHorizontalFlex()); + obj.insert("flexv", node->getVerticalFlex()); + } break; + case SplitNode::HorizontalContainer: + case SplitNode::VerticalContainer: { + obj.insert("type", node->getType() == SplitNode::HorizontalContainer ? "horizontal" + : "vertical"); + + QJsonArray items_arr; + for (const std::unique_ptr &n : node->getChildren()) { + QJsonObject subObj; + this->encodeNodeRecusively(n.get(), subObj); + items_arr.append(subObj); + } + obj.insert("items", items_arr); + } break; + } +} + void WindowManager::encodeChannel(IndirectChannel channel, QJsonObject &obj) { util::assertInGuiThread(); @@ -356,5 +400,15 @@ void WindowManager::closeAll() } } +int WindowManager::getGeneration() const +{ + return this->generation; +} + +void WindowManager::incGeneration() +{ + this->generation++; +} + } // namespace singletons } // namespace chatterino diff --git a/src/singletons/windowmanager.hpp b/src/singletons/windowmanager.hpp index 814f9231..ccee1d87 100644 --- a/src/singletons/windowmanager.hpp +++ b/src/singletons/windowmanager.hpp @@ -1,8 +1,12 @@ #pragma once +#include "widgets/splitcontainer.hpp" #include "widgets/window.hpp" namespace chatterino { +// namespace widgets { +// struct SplitContainer::Node; +//} namespace singletons { class WindowManager @@ -15,7 +19,8 @@ public: void showSettingsDialog(); void showAccountSelectPopup(QPoint point); - void layoutVisibleChatWidgets(Channel *channel = nullptr); + void layoutChannelViews(Channel *channel = nullptr); + void forceLayoutChannelViews(); void repaintVisibleChatWidgets(Channel *channel = nullptr); void repaintGifEmotes(); // void updateAll(); @@ -31,19 +36,27 @@ public: void initialize(); void closeAll(); + int getGeneration() const; + void incGeneration(); + pajlada::Signals::NoArgSignal repaintGifs; pajlada::Signals::Signal layout; private: bool initialized = false; + std::atomic generation{0}; + std::vector windows; widgets::Window *mainWindow = nullptr; widgets::Window *selectedWindow = nullptr; - void encodeChannel(IndirectChannel channel, QJsonObject &obj); - IndirectChannel decodeChannel(const QJsonObject &obj); + void encodeNodeRecusively(widgets::SplitContainer::Node *node, QJsonObject &obj); + +public: + static void encodeChannel(IndirectChannel channel, QJsonObject &obj); + static IndirectChannel decodeChannel(const QJsonObject &obj); }; } // namespace singletons diff --git a/src/util/benchmark.hpp b/src/util/benchmark.hpp index 828f6d73..c41caa41 100644 --- a/src/util/benchmark.hpp +++ b/src/util/benchmark.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #define BENCH(x) \ QElapsedTimer x; \ @@ -10,4 +11,27 @@ #define MARK(x) \ qDebug() << BOOST_CURRENT_FUNCTION << __LINE__ \ - << static_cast(x.nsecsElapsed()) / 100000.0 << "ms"; + << static_cast(x.nsecsElapsed()) / 1000000.0 << "ms"; + +class BenchmarkGuard : boost::noncopyable +{ + QElapsedTimer timer; + QString name; + +public: + BenchmarkGuard(const QString &_name) + : name(_name) + { + timer.start(); + } + + ~BenchmarkGuard() + { + qDebug() << this->name << float(timer.nsecsElapsed()) / 1000000.0f << "ms"; + } + + qreal getElapsedMs() + { + return qreal(timer.nsecsElapsed()) / 1000000.0; + } +}; diff --git a/src/util/emotemap.cpp b/src/util/emotemap.cpp new file mode 100644 index 00000000..ba003045 --- /dev/null +++ b/src/util/emotemap.cpp @@ -0,0 +1,48 @@ +#include "emotemap.hpp" + +#include "application.hpp" +#include "singletons/settingsmanager.hpp" + +namespace chatterino { +namespace util { + +EmoteData::EmoteData(messages::Image *_image) + : image1x(_image) +{ +} + +// Emotes must have a 1x image to be valid +bool EmoteData::isValid() const +{ + return this->image1x != nullptr; +} + +messages::Image *EmoteData::getImage(float scale) const +{ + int quality = getApp()->settings->preferredEmoteQuality; + + if (quality == 0) { + scale *= getApp()->settings->emoteScale.getValue(); + quality = [&] { + if (scale <= 1) + return 1; + if (scale <= 2) + return 2; + return 3; + }(); + } + + messages::Image *_image; + if (quality == 3 && this->image3x != nullptr) { + _image = this->image3x; + } else if (quality >= 2 && this->image2x != nullptr) { + _image = this->image2x; + } else { + _image = this->image1x; + } + + return _image; +} + +} // namespace util +} // namespace chatterino diff --git a/src/util/emotemap.hpp b/src/util/emotemap.hpp index 47816909..253fb44a 100644 --- a/src/util/emotemap.hpp +++ b/src/util/emotemap.hpp @@ -9,20 +9,18 @@ namespace util { struct EmoteData { EmoteData() = default; - EmoteData(messages::Image *_image) - : image1x(_image) - { - } + EmoteData(messages::Image *_image); // Emotes must have a 1x image to be valid - bool isValid() const - { - return this->image1x != nullptr; - } + bool isValid() const; + messages::Image *getImage(float scale) const; messages::Image *image1x = nullptr; messages::Image *image2x = nullptr; messages::Image *image3x = nullptr; + + // Link to the emote page i.e. https://www.frankerfacez.com/emoticon/144722-pajaCringe + QString pageLink; }; using EmoteMap = ConcurrentMap; diff --git a/src/util/irchelpers.hpp b/src/util/irchelpers.hpp index 9bf21502..fa0b559f 100644 --- a/src/util/irchelpers.hpp +++ b/src/util/irchelpers.hpp @@ -5,7 +5,7 @@ namespace chatterino { namespace util { -inline QString ParseTagString(const QString &input) +inline QString parseTagString(const QString &input) { QString output = input; output.detach(); diff --git a/src/util/mutexvalue.hpp b/src/util/mutexvalue.hpp new file mode 100644 index 00000000..9b0f37a4 --- /dev/null +++ b/src/util/mutexvalue.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include + +namespace chatterino { +namespace util { + +template +class MutexValue +{ + mutable std::mutex mutex; + T value; + +public: + MutexValue() + { + } + + MutexValue(T &&val) + : value(val) + { + } + + T get() const + { + std::lock_guard guard(this->mutex); + + return this->value; + } + + void set(const T &val) + { + std::lock_guard guard(this->mutex); + + this->value = val; + } +}; + +} // namespace util +} // namespace chatterino diff --git a/src/util/networkrequest.hpp b/src/util/networkrequest.hpp index 6131528c..11d8371a 100644 --- a/src/util/networkrequest.hpp +++ b/src/util/networkrequest.hpp @@ -216,7 +216,7 @@ public: if (this->data.caller != nullptr) { QObject::connect(worker, &NetworkWorker::doneUrl, this->data.caller, - [ onFinished, data = this->data ](auto reply) mutable { + [onFinished, data = this->data](auto reply) mutable { if (reply->error() != QNetworkReply::NetworkError::NoError) { // TODO: We might want to call an onError callback here return; @@ -238,7 +238,7 @@ public: QObject::connect( &requester, &NetworkRequester::requestUrl, worker, - [ timer, data = std::move(this->data), worker, onFinished{std::move(onFinished)} ]() { + [timer, data = std::move(this->data), worker, onFinished{std::move(onFinished)}]() { QNetworkReply *reply = NetworkManager::NaM.get(data.request); if (timer != nullptr) { @@ -253,21 +253,21 @@ public: data.onReplyCreated(reply); } - QObject::connect(reply, &QNetworkReply::finished, worker, [ - data = std::move(data), worker, reply, onFinished = std::move(onFinished) - ]() mutable { - if (data.caller == nullptr) { - QByteArray bytes = reply->readAll(); - data.writeToCache(bytes); - onFinished(bytes); + QObject::connect(reply, &QNetworkReply::finished, worker, + [data = std::move(data), worker, reply, + onFinished = std::move(onFinished)]() mutable { + if (data.caller == nullptr) { + QByteArray bytes = reply->readAll(); + data.writeToCache(bytes); + onFinished(bytes); - reply->deleteLater(); - } else { - emit worker->doneUrl(reply); - } + reply->deleteLater(); + } else { + emit worker->doneUrl(reply); + } - delete worker; - }); + delete worker; + }); }); emit requester.requestUrl(); @@ -276,7 +276,7 @@ public: template void getJSON(FinishedCallback onFinished) { - this->get([onFinished{std::move(onFinished)}](const QByteArray &bytes)->bool { + this->get([onFinished{std::move(onFinished)}](const QByteArray &bytes) -> bool { auto object = parseJSONFromData(bytes); onFinished(object); @@ -289,7 +289,7 @@ public: template void getJSON2(FinishedCallback onFinished) { - this->get([onFinished{std::move(onFinished)}](const QByteArray &bytes)->bool { + this->get([onFinished{std::move(onFinished)}](const QByteArray &bytes) -> bool { auto object = parseJSONFromData2(bytes); onFinished(object); @@ -303,6 +303,7 @@ public: { switch (this->data.requestType) { case GetRequest: { + debug::Log("Call GET request!"); this->executeGet(); } break; @@ -369,10 +370,8 @@ private: worker->moveToThread(&NetworkManager::workerThread); if (this->data.caller != nullptr) { - QObject::connect(worker, &NetworkWorker::doneUrl, - this->data.caller, [data = this->data](auto reply) mutable { - auto &dat = data; - + QObject::connect(worker, &NetworkWorker::doneUrl, this->data.caller, + [data = this->data](auto reply) mutable { if (reply->error() != QNetworkReply::NetworkError::NoError) { if (data.onError) { data.onError(reply->error()); @@ -395,8 +394,8 @@ private: } QObject::connect(&requester, &NetworkRequester::requestUrl, worker, - [ timer, data = std::move(this->data), worker ]() { - QNetworkReply *reply; + [timer, data = std::move(this->data), worker]() { + QNetworkReply *reply = nullptr; switch (data.requestType) { case GetRequest: { reply = NetworkManager::NaM.get(data.request); @@ -431,11 +430,16 @@ private: } QObject::connect(reply, &QNetworkReply::finished, worker, - [ data = std::move(data), worker, reply ]() mutable { + [data = std::move(data), worker, reply]() mutable { if (data.caller == nullptr) { QByteArray bytes = reply->readAll(); data.writeToCache(bytes); - data.onSuccess(parseJSONFromData2(bytes)); + + if (data.onSuccess) { + data.onSuccess(parseJSONFromData2(bytes)); + } else { + qWarning() << "data.onSuccess not found"; + } reply->deleteLater(); } else { diff --git a/src/util/qstringhash.hpp b/src/util/qstringhash.hpp new file mode 100644 index 00000000..d108de7c --- /dev/null +++ b/src/util/qstringhash.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include +#include + +namespace std { +template <> +struct hash { + std::size_t operator()(const QString &s) const + { + return qHash(s); + } +}; +} // namespace std diff --git a/src/util/signalvector2.hpp b/src/util/signalvector2.hpp index 8ad90d03..d26878fa 100644 --- a/src/util/signalvector2.hpp +++ b/src/util/signalvector2.hpp @@ -11,6 +11,13 @@ namespace chatterino { namespace util { +template +struct SignalVectorItemArgs { + const TVectorItem &item; + int index; + void *caller; +}; + template class ReadOnlySignalVector : boost::noncopyable { @@ -24,14 +31,8 @@ public: } virtual ~ReadOnlySignalVector() = default; - struct ItemArgs { - const TVectorItem &item; - int index; - void *caller; - }; - - pajlada::Signals::Signal itemInserted; - pajlada::Signals::Signal itemRemoved; + pajlada::Signals::Signal> itemInserted; + pajlada::Signals::Signal> itemRemoved; pajlada::Signals::NoArgSignal delayedItemsChanged; const std::vector &getVector() const @@ -69,7 +70,7 @@ public: TVectorItem item = this->vector[index]; this->vector.erase(this->vector.begin() + index); - typename ReadOnlySignalVector::ItemArgs args{item, index, caller}; + SignalVectorItemArgs args{item, index, caller}; this->itemRemoved.invoke(args); this->invokeDelayedItemsChanged(); @@ -96,7 +97,7 @@ public: this->vector.insert(this->vector.begin() + index, item); - typename ReadOnlySignalVector::ItemArgs args{item, index, caller}; + SignalVectorItemArgs args{item, index, caller}; this->itemInserted.invoke(args); this->invokeDelayedItemsChanged(); return index; @@ -107,16 +108,15 @@ template class SortedSignalVector : public BaseSignalVector { public: - virtual int insertItem(const TVectorItem &item, int proposedIndex = -1, - void *caller = 0) override + virtual int insertItem(const TVectorItem &item, int = -1, void *caller = nullptr) override { util::assertInGuiThread(); - int index = - this->vector.insert( - std::lower_bound(this->vector.begin(), this->vector.end(), item, Compare{}), item) - - this->vector.begin(); - typename ReadOnlySignalVector::ItemArgs args{item, index, caller}; + auto it = std::lower_bound(this->vector.begin(), this->vector.end(), item, Compare{}); + int index = it - this->vector.begin(); + this->vector.insert(it, item); + + SignalVectorItemArgs args{item, index, caller}; this->itemInserted.invoke(args); this->invokeDelayedItemsChanged(); return index; diff --git a/src/util/signalvectormodel.cpp b/src/util/signalvectormodel.cpp deleted file mode 100644 index 7b74a363..00000000 --- a/src/util/signalvectormodel.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "signalvectormodel.hpp" diff --git a/src/util/signalvectormodel.hpp b/src/util/signalvectormodel.hpp index 7da0637e..3ab94a26 100644 --- a/src/util/signalvectormodel.hpp +++ b/src/util/signalvectormodel.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -26,11 +27,7 @@ public: { this->vector = vec; - auto insert = [this](const typename BaseSignalVector::ItemArgs &args) { - if (args.caller == this) { - return; - } - + auto insert = [this](const SignalVectorItemArgs &args) { // get row index int index = this->getModelIndexFromVectorIndex(args.index); assert(index >= 0 && index <= this->rows.size()); @@ -43,13 +40,13 @@ public: index = this->beforeInsert(args.item, row, index); this->beginInsertRows(QModelIndex(), index, index); - this->rows.insert(this->rows.begin() + index, Row(row)); + this->rows.insert(this->rows.begin() + index, Row(row, args.item)); this->endInsertRows(); }; int i = 0; for (const TVectorItem &item : vec->getVector()) { - typename BaseSignalVector::ItemArgs args{item, i++, 0}; + SignalVectorItemArgs args{item, i++, 0}; insert(args); } @@ -57,20 +54,21 @@ public: this->managedConnect(vec->itemInserted, insert); this->managedConnect(vec->itemRemoved, [this](auto args) { - if (args.caller == this) { - return; - } - int row = this->getModelIndexFromVectorIndex(args.index); assert(row >= 0 && row <= this->rows.size()); // remove row + std::vector items = std::move(this->rows[row].items); + this->beginRemoveRows(QModelIndex(), row, row); - for (QStandardItem *item : this->rows[row].items) { - delete item; - } this->rows.erase(this->rows.begin() + row); this->endRemoveRows(); + + this->afterRemoved(args.item, items, row); + + for (QStandardItem *item : items) { + delete item; + } }); this->afterInit(); @@ -117,7 +115,10 @@ public: } else { int vecRow = this->getVectorIndexFromModelIndex(row); this->vector->removeItem(vecRow, this); - TVectorItem item = this->getItemFromRow(this->rows[row].items); + + assert(this->rows[row].original); + TVectorItem item = + this->getItemFromRow(this->rows[row].items, this->rows[row].original.get()); this->vector->insertItem(item, vecRow, this); } @@ -181,7 +182,7 @@ public: assert(row >= 0 && row < this->rows.size()); int signalVectorRow = this->getVectorIndexFromModelIndex(row); - this->vector->removeItem(signalVectorRow); + this->vector->removeItem(signalVectorRow, this); return true; } @@ -192,7 +193,8 @@ protected: } // turn a vector item into a model row - virtual TVectorItem getItemFromRow(std::vector &row) = 0; + virtual TVectorItem getItemFromRow(std::vector &row, + const TVectorItem &original) = 0; // turns a row in the model into a vector item virtual void getRowFromItem(const TVectorItem &item, std::vector &row) = 0; @@ -221,6 +223,16 @@ protected: this->endInsertRows(); } + void removeCustomRow(int index) + { + assert(index >= 0 && index <= this->rows.size()); + assert(this->rows[index].isCustomRow); + + this->beginRemoveRows(QModelIndex(), index, index); + this->rows.erase(this->rows.begin() + index); + this->endRemoveRows(); + } + std::vector createRow() { std::vector row; @@ -232,6 +244,7 @@ protected: struct Row { std::vector items; + boost::optional original; bool isCustomRow; Row(std::vector _items, bool _isCustomRow = false) @@ -239,6 +252,14 @@ protected: , isCustomRow(_isCustomRow) { } + + Row(std::vector _items, const TVectorItem &_original, + bool _isCustomRow = false) + : items(std::move(_items)) + , original(_original) + , isCustomRow(_isCustomRow) + { + } }; std::vector rows; diff --git a/src/util/urlfetch.hpp b/src/util/urlfetch.hpp index c4f1c31b..991e5a56 100644 --- a/src/util/urlfetch.hpp +++ b/src/util/urlfetch.hpp @@ -1,8 +1,8 @@ #pragma once +#include "controllers/accounts/accountcontroller.hpp" #include "credentials.hpp" #include "debug/log.hpp" -#include "singletons/accountmanager.hpp" #include "util/networkmanager.hpp" #include "util/networkrequest.hpp" @@ -100,7 +100,7 @@ static void put(QUrl url, std::function successCallback) { QNetworkRequest request(url); - auto currentTwitchUser = getApp()->accounts->Twitch.getCurrent(); + auto currentTwitchUser = getApp()->accounts->twitch.getCurrent(); QByteArray oauthToken; if (currentTwitchUser) { oauthToken = currentTwitchUser->getOAuthToken().toUtf8(); @@ -130,7 +130,7 @@ static void sendDelete(QUrl url, std::function successCallback) { QNetworkRequest request(url); - auto currentTwitchUser = getApp()->accounts->Twitch.getCurrent(); + auto currentTwitchUser = getApp()->accounts->twitch.getCurrent(); QByteArray oauthToken; if (currentTwitchUser) { oauthToken = currentTwitchUser->getOAuthToken().toUtf8(); diff --git a/src/widgets/accountpopup.cpp b/src/widgets/accountpopup.cpp index 560aa9d3..93e05e12 100644 --- a/src/widgets/accountpopup.cpp +++ b/src/widgets/accountpopup.cpp @@ -3,7 +3,6 @@ #include "application.hpp" #include "channel.hpp" #include "credentials.hpp" -#include "singletons/accountmanager.hpp" #include "singletons/settingsmanager.hpp" #include "ui_accountpopupform.h" #include "util/urlfetch.hpp" @@ -40,8 +39,8 @@ AccountPopupWidget::AccountPopupWidget(ChannelPtr _channel) connect(this, &AccountPopupWidget::refreshButtons, this, &AccountPopupWidget::actuallyRefreshButtons, Qt::QueuedConnection); - app->accounts->Twitch.currentUserChanged.connect([=] { - auto currentTwitchUser = app->accounts->Twitch.getCurrent(); + app->accounts->twitch.currentUserChanged.connect([this] { + auto currentTwitchUser = getApp()->accounts->twitch.getCurrent(); if (!currentTwitchUser) { // No twitch user set (should never happen) return; @@ -113,7 +112,7 @@ AccountPopupWidget::AccountPopupWidget(ChannelPtr _channel) }); QObject::connect(this->ui->ignore, &QPushButton::clicked, this, [=]() { - auto currentUser = getApp()->accounts->Twitch.getCurrent(); + auto currentUser = getApp()->accounts->twitch.getCurrent(); if (!this->relationship.isIgnoring()) { currentUser->ignoreByID(this->popupWidgetUser.userID, this->popupWidgetUser.username, @@ -121,11 +120,11 @@ AccountPopupWidget::AccountPopupWidget(ChannelPtr _channel) switch (result) { case IgnoreResult_Success: { this->relationship.setIgnoring(true); - emit refreshButtons(); + emit this->refreshButtons(); } break; case IgnoreResult_AlreadyIgnored: { this->relationship.setIgnoring(true); - emit refreshButtons(); + emit this->refreshButtons(); } break; case IgnoreResult_Failed: { } break; @@ -137,7 +136,7 @@ AccountPopupWidget::AccountPopupWidget(ChannelPtr _channel) switch (result) { case UnignoreResult_Success: { this->relationship.setIgnoring(false); - emit refreshButtons(); + emit this->refreshButtons(); } break; case UnignoreResult_Failed: { } break; @@ -225,12 +224,12 @@ void AccountPopupWidget::getUserData() }); auto app = getApp(); - auto currentUser = app->accounts->Twitch.getCurrent(); + auto currentUser = app->accounts->twitch.getCurrent(); currentUser->checkFollow(this->popupWidgetUser.userID, [=](auto result) { this->relationship.setFollowing(result == FollowResult_Following); - emit refreshButtons(); + emit this->refreshButtons(); }); bool isIgnoring = false; @@ -273,9 +272,9 @@ void AccountPopupWidget::loadAvatar(const QUrl &avatarUrl) void AccountPopupWidget::scaleChangedEvent(float newDpi) { this->setStyleSheet(QString("* { font-size: px; }") - .replace("", QString::number((int)(12 * newDpi)))); + .replace("", QString::number(int(12 * newDpi)))); - this->ui->lblAvatar->setFixedSize((int)(100 * newDpi), (int)(100 * newDpi)); + this->ui->lblAvatar->setFixedSize(int(100 * newDpi), int(100 * newDpi)); } void AccountPopupWidget::updateButtons(QWidget *layout, bool state) @@ -302,7 +301,7 @@ void AccountPopupWidget::sendCommand(QPushButton *button, QString command) void AccountPopupWidget::refreshLayouts() { - auto currentTwitchUser = getApp()->accounts->Twitch.getCurrent(); + auto currentTwitchUser = getApp()->accounts->twitch.getCurrent(); if (!currentTwitchUser) { // No twitch user set (should never happen) return; diff --git a/src/widgets/accountpopup.hpp b/src/widgets/accountpopup.hpp index 05d6ef0e..b35c6d16 100644 --- a/src/widgets/accountpopup.hpp +++ b/src/widgets/accountpopup.hpp @@ -19,9 +19,10 @@ class Channel; namespace widgets { -class AccountPopupWidget : public BaseWindow +class AccountPopupWidget final : public BaseWindow { Q_OBJECT + public: AccountPopupWidget(ChannelPtr _channel); diff --git a/src/widgets/accountswitchpopupwidget.cpp b/src/widgets/accountswitchpopupwidget.cpp index 922834d2..bc29116e 100644 --- a/src/widgets/accountswitchpopupwidget.cpp +++ b/src/widgets/accountswitchpopupwidget.cpp @@ -36,6 +36,8 @@ AccountSwitchPopupWidget::AccountSwitchPopupWidget(QWidget *parent) }); this->setLayout(vbox); + + // this->setStyleSheet("background: #333"); } void AccountSwitchPopupWidget::refresh() @@ -48,11 +50,12 @@ void AccountSwitchPopupWidget::focusOutEvent(QFocusEvent *) this->hide(); } -void AccountSwitchPopupWidget::paintEvent(QPaintEvent *event) +void AccountSwitchPopupWidget::paintEvent(QPaintEvent *) { QPainter painter(this); - painter.fillRect(this->rect(), QColor(255, 255, 255)); + painter.setPen(QColor("#999")); + painter.drawRect(0, 0, this->width() - 1, this->height() - 1); } } // namespace widgets diff --git a/src/widgets/accountswitchwidget.cpp b/src/widgets/accountswitchwidget.cpp index 39485b6e..ebec62ea 100644 --- a/src/widgets/accountswitchwidget.cpp +++ b/src/widgets/accountswitchwidget.cpp @@ -2,7 +2,7 @@ #include "application.hpp" #include "const.hpp" -#include "singletons/accountmanager.hpp" +#include "controllers/accounts/accountcontroller.hpp" namespace chatterino { namespace widgets { @@ -14,18 +14,18 @@ AccountSwitchWidget::AccountSwitchWidget(QWidget *parent) this->addItem(ANONYMOUS_USERNAME_LABEL); - for (const auto &userName : app->accounts->Twitch.getUsernames()) { + for (const auto &userName : app->accounts->twitch.getUsernames()) { this->addItem(userName); } - app->accounts->Twitch.userListUpdated.connect([=]() { + app->accounts->twitch.userListUpdated.connect([=]() { this->blockSignals(true); this->clear(); this->addItem(ANONYMOUS_USERNAME_LABEL); - for (const auto &userName : app->accounts->Twitch.getUsernames()) { + for (const auto &userName : app->accounts->twitch.getUsernames()) { this->addItem(userName); } @@ -40,9 +40,9 @@ AccountSwitchWidget::AccountSwitchWidget(QWidget *parent) if (!this->selectedItems().isEmpty()) { QString newUsername = this->currentItem()->text(); if (newUsername.compare(ANONYMOUS_USERNAME_LABEL, Qt::CaseInsensitive) == 0) { - app->accounts->Twitch.currentUsername = ""; + app->accounts->twitch.currentUsername = ""; } else { - app->accounts->Twitch.currentUsername = newUsername.toStdString(); + app->accounts->twitch.currentUsername = newUsername.toStdString(); } } }); @@ -61,7 +61,7 @@ void AccountSwitchWidget::refreshSelection() if (this->count() > 0) { auto app = getApp(); - auto currentUser = app->accounts->Twitch.getCurrent(); + auto currentUser = app->accounts->twitch.getCurrent(); if (currentUser->isAnon()) { this->setCurrentRow(0); diff --git a/src/widgets/attachedwindow.cpp b/src/widgets/attachedwindow.cpp index e8668e2d..36dfca81 100644 --- a/src/widgets/attachedwindow.cpp +++ b/src/widgets/attachedwindow.cpp @@ -1,14 +1,16 @@ #include "attachedwindow.hpp" #include "application.hpp" +#include "util/debugcount.hpp" +#include "widgets/split.hpp" #include #include -#include "widgets/split.hpp" - #ifdef USEWINSDK #include "Windows.h" +// don't even think about reordering these +#include "Psapi.h" #pragma comment(lib, "Dwmapi.lib") #endif @@ -17,17 +19,19 @@ namespace widgets { AttachedWindow::AttachedWindow(void *_target, int _yOffset) : QWidget(nullptr, Qt::FramelessWindowHint | Qt::Window) - , target(_target) - , yOffset(_yOffset) + , target_(_target) + , yOffset_(_yOffset) { QLayout *layout = new QVBoxLayout(this); layout->setMargin(0); this->setLayout(layout); auto *split = new Split(this); - this->ui.split = split; + this->ui_.split = split; split->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::MinimumExpanding); layout->addWidget(split); + + util::DebugCount::increase("attached window"); } AttachedWindow::~AttachedWindow() @@ -38,18 +42,51 @@ AttachedWindow::~AttachedWindow() break; } } + + util::DebugCount::decrease("attached window"); } -AttachedWindow *AttachedWindow::get(void *target, const QString &winId, int yOffset) +AttachedWindow *AttachedWindow::get(void *target, const GetArgs &args) { - for (Item &item : items) { - if (item.hwnd == target) { - return item.window; + AttachedWindow *window = [&]() { + for (Item &item : items) { + if (item.hwnd == target) { + return item.window; + } + } + + auto *window = new AttachedWindow(target, args.yOffset); + items.push_back(Item{target, window, args.winId}); + return window; + }(); + + bool show = true; + QSize size = window->size(); + + if (args.height != -1) { + if (args.height == 0) { + window->hide(); + show = false; + } else { + window->height_ = args.height; + size.setHeight(args.height); + } + } + if (args.width != -1) { + if (args.width == 0) { + window->hide(); + show = false; + } else { + window->width_ = args.width; + size.setWidth(args.width); } } - auto *window = new AttachedWindow(target, yOffset); - items.push_back(Item{target, window, winId}); + if (show) { + window->updateWindowRect_(window->target_); + window->show(); + } + return window; } @@ -64,43 +101,92 @@ void AttachedWindow::detach(const QString &winId) void AttachedWindow::setChannel(ChannelPtr channel) { - this->ui.split->setChannel(channel); + this->ui_.split->setChannel(channel); } void AttachedWindow::showEvent(QShowEvent *) { - attachToHwnd(this->target); + attachToHwnd_(this->target_); } -void AttachedWindow::attachToHwnd(void *_hwnd) +void AttachedWindow::attachToHwnd_(void *_attachedPtr) { #ifdef USEWINSDK - QTimer *timer = new QTimer(this); - timer->setInterval(1); + if (this->attached_) { + return; + } - HWND hwnd = (HWND)this->winId(); - HWND attached = (HWND)_hwnd; - QObject::connect(timer, &QTimer::timeout, [this, hwnd, attached, timer] { - ::SetLastError(0); - RECT xD; - ::GetWindowRect(attached, &xD); + this->attached_ = true; + this->timer_.setInterval(1); - if (::GetLastError() != 0) { - timer->stop(); - timer->deleteLater(); - this->deleteLater(); + auto hwnd = HWND(this->winId()); + auto attached = HWND(_attachedPtr); + + QObject::connect(&this->timer_, &QTimer::timeout, [this, hwnd, attached] { + // check process id + if (!this->validProcessName_) { + DWORD processId; + ::GetWindowThreadProcessId(attached, &processId); + + HANDLE process = + ::OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, processId); + + std::unique_ptr filename(new TCHAR[512]); + DWORD filenameLength = ::GetModuleFileNameEx(process, nullptr, filename.get(), 512); + QString qfilename = QString::fromWCharArray(filename.get(), filenameLength); + + if (!qfilename.endsWith("chrome.exe")) { + qDebug() << "NM Illegal caller" << qfilename; + this->timer_.stop(); + this->deleteLater(); + return; + } + this->validProcessName_ = true; } - HWND next = ::GetNextWindow(attached, GW_HWNDPREV); - - ::SetWindowPos(hwnd, next ? next : HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); - ::MoveWindow(hwnd, xD.right - 360, xD.top + this->yOffset - 8, 360 - 8, - xD.bottom - xD.top - this->yOffset, false); - // ::MoveWindow(hwnd, xD.right - 360, xD.top + 82, 360 - 8, xD.bottom - xD.top - 82 - - // 8, - // false); + this->updateWindowRect_(attached); }); - timer->start(); + this->timer_.start(); +#endif +} + +void AttachedWindow::updateWindowRect_(void *_attachedPtr) +{ +#ifdef USEWINSDK + auto hwnd = HWND(this->winId()); + auto attached = HWND(_attachedPtr); + + // We get the window rect first so we can close this window when it returns an error. + // If we query the process first and check the filename then it will return and empty string + // that doens't match. + ::SetLastError(0); + RECT rect; + ::GetWindowRect(attached, &rect); + + if (::GetLastError() != 0) { + qDebug() << "NM GetLastError()" << ::GetLastError(); + + this->timer_.stop(); + this->deleteLater(); + return; + } + + // set the correct z-order + HWND next = ::GetNextWindow(attached, GW_HWNDPREV); + + ::SetWindowPos(hwnd, next ? next : HWND_TOPMOST, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); + + if (this->height_ == -1) { + // ::MoveWindow(hwnd, rect.right - this->width_ - 8, rect.top + this->yOffset_ - 8, + // this->width_, rect.bottom - rect.top - this->yOffset_, false); + } else { + ::MoveWindow(hwnd, rect.right - this->width_ - 8, rect.bottom - this->height_ - 8, + this->width_, this->height_, false); + } + + // ::MoveWindow(hwnd, rect.right - 360, rect.top + 82, 360 - 8, rect.bottom - + // rect.top - 82 - 8, false); #endif } diff --git a/src/widgets/attachedwindow.hpp b/src/widgets/attachedwindow.hpp index a161f416..8d7c216c 100644 --- a/src/widgets/attachedwindow.hpp +++ b/src/widgets/attachedwindow.hpp @@ -10,12 +10,19 @@ namespace widgets { class AttachedWindow : public QWidget { - AttachedWindow(void *target, int asdf); + AttachedWindow(void *target_, int asdf); public: - ~AttachedWindow(); + struct GetArgs { + QString winId; + int yOffset = -1; + int width = -1; + int height = -1; + }; - static AttachedWindow *get(void *target, const QString &winId, int yOffset); + virtual ~AttachedWindow() override; + + static AttachedWindow *get(void *target_, const GetArgs &args); static void detach(const QString &winId); void setChannel(ChannelPtr channel); @@ -26,14 +33,21 @@ protected: // override; private: - void *target; - int yOffset; + void *target_; + int yOffset_; + int currentYOffset_; + int width_ = 360; + int height_ = -1; + bool validProcessName_ = false; + bool attached_ = false; + QTimer timer_; struct { Split *split; - } ui; + } ui_; - void attachToHwnd(void *hwnd); + void attachToHwnd_(void *attached); + void updateWindowRect_(void *attached); struct Item { void *hwnd; diff --git a/src/widgets/basewidget.cpp b/src/widgets/basewidget.cpp index 4dcc1a6d..5a1c5b0c 100644 --- a/src/widgets/basewidget.cpp +++ b/src/widgets/basewidget.cpp @@ -27,7 +27,6 @@ BaseWidget::~BaseWidget() float BaseWidget::getScale() const { - // return 1.f; BaseWidget *baseWidget = dynamic_cast(this->window()); if (baseWidget == nullptr) { diff --git a/src/widgets/basewidget.hpp b/src/widgets/basewidget.hpp index 998befca..f6967600 100644 --- a/src/widgets/basewidget.hpp +++ b/src/widgets/basewidget.hpp @@ -18,7 +18,7 @@ class BaseWidget : public QWidget public: explicit BaseWidget(QWidget *parent, Qt::WindowFlags f = Qt::WindowFlags()); - virtual ~BaseWidget(); + virtual ~BaseWidget() override; float getScale() const; pajlada::Signals::Signal scaleChanged; @@ -26,7 +26,7 @@ public: QSize getScaleIndependantSize() const; int getScaleIndependantWidth() const; int getScaleIndependantHeight() const; - void setScaleIndependantSize(int width, int yOffset); + void setScaleIndependantSize(int width, int height); void setScaleIndependantSize(QSize); void setScaleIndependantWidth(int value); void setScaleIndependantHeight(int value); diff --git a/src/widgets/basewindow.cpp b/src/widgets/basewindow.cpp index 65408b5d..c832597f 100644 --- a/src/widgets/basewindow.cpp +++ b/src/widgets/basewindow.cpp @@ -32,17 +32,28 @@ namespace chatterino { namespace widgets { -BaseWindow::BaseWindow(QWidget *parent, bool _enableCustomFrame) - : BaseWidget(parent, Qt::Window) - , enableCustomFrame(_enableCustomFrame) +BaseWindow::BaseWindow(QWidget *parent, Flags _flags) + : BaseWidget(parent, + Qt::Window | ((_flags & TopMost) ? Qt::WindowStaysOnTopHint : (Qt::WindowFlags)0)) + , enableCustomFrame(_flags & EnableCustomFrame) + , frameless(_flags & FrameLess) + , flags(_flags) { + if (this->frameless) { + this->enableCustomFrame = false; + this->setWindowFlag(Qt::FramelessWindowHint); + } + this->init(); } +BaseWindow::Flags BaseWindow::getFlags() +{ + return this->flags; +} + void BaseWindow::init() { - auto app = getApp(); - this->setWindowIcon(QIcon(":/images/icon.png")); #ifdef USEWINSDK @@ -53,55 +64,57 @@ void BaseWindow::init() layout->setSpacing(0); this->setLayout(layout); { - QHBoxLayout *buttonLayout = this->ui.titlebarBox = new QHBoxLayout(); - buttonLayout->setMargin(0); - layout->addLayout(buttonLayout); + if (!this->frameless) { + QHBoxLayout *buttonLayout = this->ui.titlebarBox = new QHBoxLayout(); + buttonLayout->setMargin(0); + layout->addLayout(buttonLayout); - // title - QLabel *title = new QLabel(" Chatterino"); - QObject::connect(this, &QWidget::windowTitleChanged, - [title](const QString &text) { title->setText(" " + text); }); + // title + QLabel *title = new QLabel(" Chatterino"); + QObject::connect(this, &QWidget::windowTitleChanged, + [title](const QString &text) { title->setText(" " + text); }); - QSizePolicy policy(QSizePolicy::Ignored, QSizePolicy::Preferred); - policy.setHorizontalStretch(1); - // title->setBaseSize(0, 0); - title->setScaledContents(true); - title->setSizePolicy(policy); - buttonLayout->addWidget(title); - this->ui.titleLabel = title; + QSizePolicy policy(QSizePolicy::Ignored, QSizePolicy::Preferred); + policy.setHorizontalStretch(1); + // title->setBaseSize(0, 0); + title->setScaledContents(true); + title->setSizePolicy(policy); + buttonLayout->addWidget(title); + this->ui.titleLabel = title; - // buttons - TitleBarButton *_minButton = new TitleBarButton; - _minButton->setButtonStyle(TitleBarButton::Minimize); - TitleBarButton *_maxButton = new TitleBarButton; - _maxButton->setButtonStyle(TitleBarButton::Maximize); - TitleBarButton *_exitButton = new TitleBarButton; - _exitButton->setButtonStyle(TitleBarButton::Close); + // buttons + TitleBarButton *_minButton = new TitleBarButton; + _minButton->setButtonStyle(TitleBarButton::Minimize); + TitleBarButton *_maxButton = new TitleBarButton; + _maxButton->setButtonStyle(TitleBarButton::Maximize); + TitleBarButton *_exitButton = new TitleBarButton; + _exitButton->setButtonStyle(TitleBarButton::Close); - QObject::connect(_minButton, &TitleBarButton::clicked, this, [this] { - this->setWindowState(Qt::WindowMinimized | this->windowState()); - }); - QObject::connect(_maxButton, &TitleBarButton::clicked, this, [this] { - this->setWindowState(this->windowState() == Qt::WindowMaximized - ? Qt::WindowActive - : Qt::WindowMaximized); - }); - QObject::connect(_exitButton, &TitleBarButton::clicked, this, - [this] { this->close(); }); + QObject::connect(_minButton, &TitleBarButton::clicked, this, [this] { + this->setWindowState(Qt::WindowMinimized | this->windowState()); + }); + QObject::connect(_maxButton, &TitleBarButton::clicked, this, [this] { + this->setWindowState(this->windowState() == Qt::WindowMaximized + ? Qt::WindowActive + : Qt::WindowMaximized); + }); + QObject::connect(_exitButton, &TitleBarButton::clicked, this, + [this] { this->close(); }); - this->ui.minButton = _minButton; - this->ui.maxButton = _maxButton; - this->ui.exitButton = _exitButton; + this->ui.minButton = _minButton; + this->ui.maxButton = _maxButton; + this->ui.exitButton = _exitButton; - this->ui.buttons.push_back(_minButton); - this->ui.buttons.push_back(_maxButton); - this->ui.buttons.push_back(_exitButton); + this->ui.buttons.push_back(_minButton); + this->ui.buttons.push_back(_maxButton); + this->ui.buttons.push_back(_exitButton); - // buttonLayout->addStretch(1); - buttonLayout->addWidget(_minButton); - buttonLayout->addWidget(_maxButton); - buttonLayout->addWidget(_exitButton); - buttonLayout->setSpacing(0); + // buttonLayout->addStretch(1); + buttonLayout->addWidget(_minButton); + buttonLayout->addWidget(_maxButton); + buttonLayout->addWidget(_exitButton); + buttonLayout->setSpacing(0); + } } this->ui.layoutBase = new BaseWidget(this); layout->addWidget(this->ui.layoutBase); @@ -115,9 +128,21 @@ void BaseWindow::init() } #endif - if (app->settings->windowTopMost.getValue()) { - this->setWindowFlags(this->windowFlags() | Qt::WindowStaysOnTopHint); +#ifdef USEWINSDK + // fourtf: don't ask me why we need to delay this + if (!(this->flags & Flags::TopMost)) { + QTimer::singleShot(1, this, [this] { + getApp()->settings->windowTopMost.connect([this](bool topMost, auto) { + ::SetWindowPos(HWND(this->winId()), topMost ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, + 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); + }); + }); } +#else +// if (getApp()->settings->windowTopMost.getValue()) { +// this->setWindowFlag(Qt::WindowStaysOnTopHint); +// } +#endif } void BaseWindow::setStayInScreenRect(bool value) @@ -158,10 +183,12 @@ void BaseWindow::themeRefreshEvent() palette.setColor(QPalette::Foreground, this->themeManager->window.text); this->setPalette(palette); - QPalette palette_title; - palette_title.setColor(QPalette::Foreground, - this->themeManager->isLightTheme() ? "#333" : "#ccc"); - this->ui.titleLabel->setPalette(palette_title); + if (this->ui.titleLabel) { + QPalette palette_title; + palette_title.setColor(QPalette::Foreground, + this->themeManager->isLightTheme() ? "#333" : "#ccc"); + this->ui.titleLabel->setPalette(palette_title); + } for (RippleEffectButton *button : this->ui.buttons) { button->setMouseEffectColor(this->themeManager->window.text); @@ -205,7 +232,7 @@ void BaseWindow::changeEvent(QEvent *) TooltipWidget::getInstance()->hide(); #ifdef USEWINSDK - if (this->hasCustomWindowFrame()) { + if (this->ui.maxButton) { this->ui.maxButton->setButtonStyle(this->windowState() & Qt::WindowMaximized ? TitleBarButton::Unmaximize : TitleBarButton::Maximize); @@ -222,10 +249,12 @@ void BaseWindow::leaveEvent(QEvent *) TooltipWidget::getInstance()->hide(); } -void BaseWindow::moveTo(QWidget *parent, QPoint point) +void BaseWindow::moveTo(QWidget *parent, QPoint point, bool offset) { - point.rx() += 16; - point.ry() += 16; + if (offset) { + point.rx() += 16; + point.ry() += 16; + } this->move(point); this->moveIntoDesktopRect(parent); @@ -295,10 +324,18 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message, long *r NCCALCSIZE_PARAMS *ncp = (reinterpret_cast(msg->lParam)); ncp->lppos->flags |= SWP_NOREDRAW; RECT *clientRect = &ncp->rgrc[0]; - clientRect->left += cx; - clientRect->top += 0; - clientRect->right -= cx; - clientRect->bottom -= cy; + + if (IsWindows10OrGreater()) { + clientRect->left += cx; + clientRect->top += 0; + clientRect->right -= cx; + clientRect->bottom -= cy; + } else { + clientRect->left += 1; + clientRect->top += 0; + clientRect->right -= 1; + clientRect->bottom -= 1; + } } *result = 0; @@ -306,14 +343,13 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message, long *r } else { return QWidget::nativeEvent(eventType, message, result); } - break; - } + } break; case WM_NCHITTEST: { if (this->hasCustomWindowFrame()) { *result = 0; const LONG border_width = 8; // in pixels RECT winrect; - GetWindowRect((HWND)winId(), &winrect); + GetWindowRect(HWND(winId()), &winrect); long x = GET_X_LPARAM(msg->lParam); long y = GET_Y_LPARAM(msg->lParam); @@ -400,17 +436,18 @@ void BaseWindow::showEvent(QShowEvent *event) { if (!this->shown && this->isVisible() && this->hasCustomWindowFrame()) { this->shown = true; - SetWindowLongPtr((HWND)this->winId(), GWL_STYLE, - WS_POPUP | WS_CAPTION | WS_THICKFRAME | WS_MAXIMIZEBOX | WS_MINIMIZEBOX); + // SetWindowLongPtr((HWND)this->winId(), GWL_STYLE, + // WS_POPUP | WS_CAPTION | WS_THICKFRAME | WS_MAXIMIZEBOX | + // WS_MINIMIZEBOX); const MARGINS shadow = {8, 8, 8, 8}; - DwmExtendFrameIntoClientArea((HWND)this->winId(), &shadow); + DwmExtendFrameIntoClientArea(HWND(this->winId()), &shadow); } BaseWidget::showEvent(event); } -void BaseWindow::paintEvent(QPaintEvent *event) +void BaseWindow::paintEvent(QPaintEvent *) { if (this->hasCustomWindowFrame()) { QPainter painter(this); @@ -434,13 +471,19 @@ void BaseWindow::calcButtonsSizes() return; } if ((this->width() / this->getScale()) < 300) { - this->ui.minButton->setScaleIndependantSize(30, 30); - this->ui.maxButton->setScaleIndependantSize(30, 30); - this->ui.exitButton->setScaleIndependantSize(30, 30); + if (this->ui.minButton) + this->ui.minButton->setScaleIndependantSize(30, 30); + if (this->ui.maxButton) + this->ui.maxButton->setScaleIndependantSize(30, 30); + if (this->ui.exitButton) + this->ui.exitButton->setScaleIndependantSize(30, 30); } else { - this->ui.minButton->setScaleIndependantSize(46, 30); - this->ui.maxButton->setScaleIndependantSize(46, 30); - this->ui.exitButton->setScaleIndependantSize(46, 30); + if (this->ui.minButton) + this->ui.minButton->setScaleIndependantSize(46, 30); + if (this->ui.maxButton) + this->ui.maxButton->setScaleIndependantSize(46, 30); + if (this->ui.exitButton) + this->ui.exitButton->setScaleIndependantSize(46, 30); } } } // namespace widgets diff --git a/src/widgets/basewindow.hpp b/src/widgets/basewindow.hpp index c1a65afb..ec5776b6 100644 --- a/src/widgets/basewindow.hpp +++ b/src/widgets/basewindow.hpp @@ -19,7 +19,9 @@ class BaseWindow : public BaseWidget Q_OBJECT public: - explicit BaseWindow(QWidget *parent = nullptr, bool enableCustomFrame = false); + enum Flags { None = 0, EnableCustomFrame = 1, FrameLess = 2, TopMost = 4 }; + + explicit BaseWindow(QWidget *parent = nullptr, Flags flags = None); QWidget *getLayoutContainer(); bool hasCustomWindowFrame(); @@ -29,7 +31,9 @@ public: void setStayInScreenRect(bool value); bool getStayInScreenRect() const; - void moveTo(QWidget *widget, QPoint point); + void moveTo(QWidget *widget, QPoint point, bool offset = true); + + Flags getFlags(); protected: #ifdef USEWINSDK @@ -51,16 +55,18 @@ private: void calcButtonsSizes(); bool enableCustomFrame; + bool frameless; bool stayInScreenRect = false; bool shown = false; + Flags flags; struct { - QHBoxLayout *titlebarBox; - QWidget *titleLabel; + QHBoxLayout *titlebarBox = nullptr; + QWidget *titleLabel = nullptr; TitleBarButton *minButton = nullptr; TitleBarButton *maxButton = nullptr; TitleBarButton *exitButton = nullptr; - QWidget *layoutBase; + QWidget *layoutBase = nullptr; std::vector buttons; } ui; }; diff --git a/src/widgets/emotepopup.cpp b/src/widgets/emotepopup.cpp index 85496578..4449cc87 100644 --- a/src/widgets/emotepopup.cpp +++ b/src/widgets/emotepopup.cpp @@ -1,12 +1,13 @@ #include "emotepopup.hpp" #include "application.hpp" +#include "controllers/accounts/accountcontroller.hpp" #include "messages/messagebuilder.hpp" #include "providers/twitch/twitchchannel.hpp" -#include "singletons/accountmanager.hpp" #include "widgets/notebook.hpp" #include +#include #include using namespace chatterino::providers::twitch; @@ -16,14 +17,14 @@ namespace chatterino { namespace widgets { EmotePopup::EmotePopup() - : BaseWindow(nullptr, true) + : BaseWindow(nullptr, BaseWindow::EnableCustomFrame) { this->viewEmotes = new ChannelView(); this->viewEmojis = new ChannelView(); - this->viewEmotes->setOverrideFlags((MessageElement::Flags)( + this->viewEmotes->setOverrideFlags(MessageElement::Flags( MessageElement::Default | MessageElement::AlwaysShow | MessageElement::EmoteImages)); - this->viewEmojis->setOverrideFlags((MessageElement::Flags)( + this->viewEmojis->setOverrideFlags(MessageElement::Flags( MessageElement::Default | MessageElement::AlwaysShow | MessageElement::EmoteImages)); this->viewEmotes->setEnableScrollingToBottom(false); @@ -32,7 +33,7 @@ EmotePopup::EmotePopup() auto *layout = new QVBoxLayout(this); this->getLayoutContainer()->setLayout(layout); - Notebook2 *notebook = new Notebook2(this); + Notebook *notebook = new Notebook(this); layout->addWidget(notebook); layout->setMargin(0); @@ -83,10 +84,36 @@ void EmotePopup::loadChannel(ChannelPtr _channel) auto app = getApp(); - QString userID = app->accounts->Twitch.getCurrent()->getUserId(); + QString userID = app->accounts->twitch.getCurrent()->getUserId(); + + // fourtf: the entire emote manager needs to be refactored so there's no point in trying to + // fix this pile of garbage + for (const auto &set : app->emotes->twitchAccountEmotes[userID.toStdString()].emoteSets) { + // TITLE + messages::MessageBuilder builder1; + + builder1.append(new TextElement("Twitch Account Emotes", MessageElement::Text)); + + builder1.getMessage()->flags |= Message::Centered; + emoteChannel->addMessage(builder1.getMessage()); + + // EMOTES + messages::MessageBuilder builder2; + builder2.getMessage()->flags |= Message::Centered; + builder2.getMessage()->flags |= Message::DisableCompactEmotes; + + for (const auto &emote : set.second) { + [&](const QString &key, const util::EmoteData &value) { + builder2.append((new EmoteElement(value, MessageElement::Flags::AlwaysShow)) + ->setLink(Link(Link::InsertText, key))); + }(QString::fromStdString(emote.code), + app->emotes->getTwitchEmoteById(QString::fromStdString(emote.id).toLong(), + QString::fromStdString(emote.code))); + } + + emoteChannel->addMessage(builder2.getMessage()); + } - addEmotes(app->emotes->twitchAccountEmotes[userID.toStdString()].emotes, - "Twitch Account Emotes", "Twitch Account Emote"); addEmotes(app->emotes->bttvGlobalEmotes, "BetterTTV Global Emotes", "BetterTTV Global Emote"); addEmotes(*channel->bttvChannelEmotes.get(), "BetterTTV Channel Emotes", "BetterTTV Channel Emote"); diff --git a/src/widgets/helper/channelview.cpp b/src/widgets/helper/channelview.cpp index 95055e6b..de7af454 100644 --- a/src/widgets/helper/channelview.cpp +++ b/src/widgets/helper/channelview.cpp @@ -27,7 +27,10 @@ #include #include -#define LAYOUT_WIDTH (this->width() - (this->scrollBar.isVisible() ? 16 : 4) * this->getScale()) +#define LAYOUT_WIDTH (this->width() - (this->scrollBar.isVisible() ? 16 : 2) * this->getScale()) +#define DRAW_WIDTH (this->width()) +#define SELECTION_RESUME_SCROLLING_MSG_THRESHOLD 3 +#define CHAT_HOVER_PAUSE_DURATION 400 using namespace chatterino::messages; using namespace chatterino::providers::twitch; @@ -42,9 +45,6 @@ ChannelView::ChannelView(BaseWidget *parent) { auto app = getApp(); -#ifndef Q_OS_MAC -// this->setAttribute(Qt::WA_OpaquePaintEvent); -#endif this->setMouseTracking(true); this->managedConnections.emplace_back(app->settings->wordFlagsChanged.connect([=] { @@ -55,8 +55,12 @@ ChannelView::ChannelView(BaseWidget *parent) this->scrollBar.getCurrentValueChanged().connect([this] { // Whenever the scrollbar value has been changed, re-render the ChatWidgetView this->actuallyLayoutMessages(true); - this->goToBottom->setVisible(this->enableScrollingToBottom && this->scrollBar.isVisible() && - !this->scrollBar.isAtBottom()); + + if (!this->isPaused()) { + this->goToBottom->setVisible(this->enableScrollingToBottom && + this->scrollBar.isVisible() && + !this->scrollBar.isAtBottom()); + } this->queueUpdate(); }); @@ -97,12 +101,10 @@ ChannelView::ChannelView(BaseWidget *parent) // }); this->pauseTimeout.setSingleShot(true); - - // auto e = new QResizeEvent(this->size(), this->size()); - // this->resizeEvent(e); - // delete e; - - this->scrollBar.resize(this->scrollBar.width(), this->height() + 1); + QObject::connect(&this->pauseTimeout, &QTimer::timeout, [this] { + this->pausedTemporarily = false; + this->layoutMessages(); + }); app->settings->showLastMessageIndicator.connect( [this](auto, auto) { @@ -120,6 +122,15 @@ ChannelView::ChannelView(BaseWidget *parent) this->layoutQueued = false; } }); + + QTimer::singleShot(1000, this, [this] { + this->scrollBar.setGeometry(this->width() - this->scrollBar.width(), 0, + this->scrollBar.width(), this->height()); + }); + + QShortcut *shortcut = new QShortcut(QKeySequence("Ctrl+C"), this); + QObject::connect(shortcut, &QShortcut::activated, + [this] { QGuiApplication::clipboard()->setText(this->getSelectedText()); }); } ChannelView::~ChannelView() @@ -165,9 +176,10 @@ void ChannelView::layoutMessages() void ChannelView::actuallyLayoutMessages(bool causedByScrollbar) { + // BenchmarkGuard benchmark("layout messages"); + auto app = getApp(); - // BENCH(timer) auto messagesSnapshot = this->getMessagesSnapshot(); if (messagesSnapshot.getLength() == 0) { @@ -246,13 +258,14 @@ void ChannelView::actuallyLayoutMessages(bool causedByScrollbar) // Perhaps also if the user scrolled with the scrollwheel in this ChatWidget in the last 0.2 // seconds or something if (this->enableScrollingToBottom && this->showingLatestMessages && showScrollbar) { - this->scrollBar.scrollToBottom(this->messageWasAdded && - app->settings->enableSmoothScrollingNewMessages.getValue()); + if (!this->isPaused()) { + this->scrollBar.scrollToBottom( + // this->messageWasAdded && + app->settings->enableSmoothScrollingNewMessages.getValue()); + } this->messageWasAdded = false; } - // MARK(timer); - if (redrawRequired) { this->queueUpdate(); } @@ -336,10 +349,11 @@ const boost::optional &ChannelView::getOverride messages::LimitedQueueSnapshot ChannelView::getMessagesSnapshot() { - if (!this->paused) { - this->snapshot = this->messages.getSnapshot(); - } + // if (!this->isPaused()) { + this->snapshot = this->messages.getSnapshot(); + // } + // return this->snapshot; return this->snapshot; } @@ -363,14 +377,18 @@ void ChannelView::setChannel(ChannelPtr newChannel) } this->lastMessageHasAlternateBackground = !this->lastMessageHasAlternateBackground; + if (this->isPaused()) { + this->messagesAddedSinceSelectionPause++; + } + if (this->messages.pushBack(MessageLayoutPtr(messageRef), deleted)) { - if (!this->paused) { - if (this->scrollBar.isAtBottom()) { - this->scrollBar.scrollToBottom(); - } else { - this->scrollBar.offset(-1); - } + // if (!this->isPaused()) { + if (this->scrollBar.isAtBottom()) { + this->scrollBar.scrollToBottom(); + } else { + this->scrollBar.offset(-1); } + // } } if (!(message->flags & Message::DoNotTriggerNotification)) { @@ -395,7 +413,7 @@ void ChannelView::setChannel(ChannelPtr newChannel) messageRefs.at(i) = MessageLayoutPtr(new MessageLayout(messages.at(i))); } - if (!this->paused) { + if (!this->isPaused()) { if (this->messages.pushFront(messageRefs).size() > 0) { if (this->scrollBar.isAtBottom()) { this->scrollBar.scrollToBottom(); @@ -474,7 +492,7 @@ void ChannelView::detachChannel() void ChannelView::pause(int msecTimeout) { - this->paused = true; + this->pausedTemporarily = true; this->pauseTimeout.start(msecTimeout); } @@ -492,8 +510,8 @@ void ChannelView::updateLastReadMessage() void ChannelView::resizeEvent(QResizeEvent *) { - this->scrollBar.resize(this->scrollBar.width(), this->height()); - this->scrollBar.move(this->width() - this->scrollBar.width(), 0); + this->scrollBar.setGeometry(this->width() - this->scrollBar.width(), 0, this->scrollBar.width(), + this->height()); this->goToBottom->setGeometry(0, this->height() - 32, this->width(), 32); @@ -507,6 +525,13 @@ void ChannelView::resizeEvent(QResizeEvent *) void ChannelView::setSelection(const SelectionItem &start, const SelectionItem &end) { // selections + if (!this->selecting && start != end) { + this->messagesAddedSinceSelectionPause = 0; + + this->selecting = true; + this->pausedBySelection = true; + } + this->selection = Selection(start, end); this->selectionChanged.invoke(); @@ -536,9 +561,26 @@ messages::MessageElement::Flags ChannelView::getFlags() const return flags; } +bool ChannelView::isPaused() +{ + return this->pausedTemporarily || this->pausedBySelection; +} + +// void ChannelView::beginPause() +//{ +// if (this->scrollBar.isAtBottom()) { +// this->scrollBar.setDesiredValue(this->scrollBar.getDesiredValue() - 0.001); +// this->layoutMessages(); +// } +//} + +// void ChannelView::endPause() +//{ +//} + void ChannelView::paintEvent(QPaintEvent * /*event*/) { - // BENCH(timer); + // BenchmarkGuard benchmark("paint event"); QPainter painter(this); @@ -546,8 +588,6 @@ void ChannelView::paintEvent(QPaintEvent * /*event*/) // draw messages this->drawMessages(painter); - - // MARK(timer); } // if overlays is false then it draws the message, if true then it draws things such as the grey @@ -558,14 +598,14 @@ void ChannelView::drawMessages(QPainter &painter) auto messagesSnapshot = this->getMessagesSnapshot(); - size_t start = this->scrollBar.getCurrentValue(); + size_t start = size_t(this->scrollBar.getCurrentValue()); if (start >= messagesSnapshot.getLength()) { return; } - int y = -(messagesSnapshot[start].get()->getHeight() * - (fmod(this->scrollBar.getCurrentValue(), 1))); + int y = int(-(messagesSnapshot[start].get()->getHeight() * + (fmod(this->scrollBar.getCurrentValue(), 1)))); messages::MessageLayout *end = nullptr; bool windowFocused = this->window() == QApplication::activeWindow(); @@ -578,7 +618,7 @@ void ChannelView::drawMessages(QPainter &painter) isLastMessage = this->lastReadMessage.get() == layout; } - layout->paint(painter, y, i, this->selection, isLastMessage, windowFocused); + layout->paint(painter, DRAW_WIDTH, y, i, this->selection, isLastMessage, windowFocused); y += layout->getHeight(); @@ -622,6 +662,9 @@ void ChannelView::drawMessages(QPainter &painter) void ChannelView::wheelEvent(QWheelEvent *event) { + this->pausedBySelection = false; + this->pausedTemporarily = false; + if (this->scrollBar.isVisible()) { auto app = getApp(); @@ -691,7 +734,8 @@ void ChannelView::enterEvent(QEvent *) void ChannelView::leaveEvent(QEvent *) { - this->paused = false; + this->pausedTemporarily = false; + this->layoutMessages(); } void ChannelView::mouseMoveEvent(QMouseEvent *event) @@ -706,7 +750,7 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event) auto app = getApp(); if (app->settings->pauseChatHover.getValue()) { - this->pause(300); + this->pause(CHAT_HOVER_PAUSE_DURATION); } auto tooltipWidget = TooltipWidget::getInstance(); @@ -722,8 +766,8 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event) } // is selecting - if (this->selecting) { - this->pause(500); + if (this->isMouseDown) { + this->pause(300); int index = layout->getSelectionIndex(relativePos); this->setSelection(this->selection.start, SelectionItem(messageIndex, index)); @@ -732,7 +776,7 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event) } // message under cursor is collapsed - if (layout->getMessage()->flags & Message::Collapsed) { + if (layout->flags & MessageLayout::Collapsed) { this->setCursor(Qt::PointingHandCursor); tooltipWidget->hide(); return; @@ -754,6 +798,7 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event) tooltipWidget->moveTo(this, event->globalPos()); tooltipWidget->setText(tooltip); tooltipWidget->show(); + tooltipWidget->raise(); } // check if word has a link @@ -766,29 +811,14 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event) void ChannelView::mousePressEvent(QMouseEvent *event) { - if (event->modifiers() & (Qt::AltModifier | Qt::ControlModifier)) { - this->unsetCursor(); - - event->ignore(); - return; - } - auto app = getApp(); - if (app->settings->linksDoubleClickOnly.getValue()) { - this->pause(200); - } - - this->isMouseDown = true; - - this->lastPressPosition = event->screenPos(); + this->mouseDown.invoke(event); std::shared_ptr layout; QPoint relativePos; int messageIndex; - this->mouseDown.invoke(event); - if (!tryGetMessageAt(event->pos(), layout, relativePos, messageIndex)) { setCursor(Qt::ArrowCursor); @@ -798,71 +828,87 @@ void ChannelView::mousePressEvent(QMouseEvent *event) } // Start selection at the last message at its last index - auto lastMessageIndex = messagesSnapshot.getLength() - 1; - auto lastMessage = messagesSnapshot[lastMessageIndex]; - auto lastCharacterIndex = lastMessage->getLastCharacterIndex(); + if (event->button() == Qt::LeftButton) { + auto lastMessageIndex = messagesSnapshot.getLength() - 1; + auto lastMessage = messagesSnapshot[lastMessageIndex]; + auto lastCharacterIndex = lastMessage->getLastCharacterIndex(); - SelectionItem selectionItem(lastMessageIndex, lastCharacterIndex); - this->setSelection(selectionItem, selectionItem); - this->selecting = true; + SelectionItem selectionItem(lastMessageIndex, lastCharacterIndex); + this->setSelection(selectionItem, selectionItem); + } return; } // check if message is collapsed - if (layout->getMessage()->flags & Message::Collapsed) { + if (layout->flags & MessageLayout::Collapsed) { return; } - int index = layout->getSelectionIndex(relativePos); + switch (event->button()) { + case Qt::LeftButton: { + if (app->settings->linksDoubleClickOnly.getValue()) { + this->pause(200); + } - auto selectionItem = SelectionItem(messageIndex, index); - this->setSelection(selectionItem, selectionItem); - this->selecting = true; + this->lastPressPosition = event->screenPos(); + this->isMouseDown = true; - this->repaint(); + int index = layout->getSelectionIndex(relativePos); + + auto selectionItem = SelectionItem(messageIndex, index); + this->setSelection(selectionItem, selectionItem); + } break; + + case Qt::RightButton: { + this->lastRightPressPosition = event->screenPos(); + this->isRightMouseDown = true; + } break; + + default:; + } + + this->update(); } void ChannelView::mouseReleaseEvent(QMouseEvent *event) { - if (event->modifiers() & (Qt::AltModifier | Qt::ControlModifier)) { - this->unsetCursor(); + // check if mouse was pressed + if (event->button() == Qt::LeftButton) { + if (this->isMouseDown) { + this->isMouseDown = false; - event->ignore(); + if (fabsf(util::distanceBetweenPoints(this->lastPressPosition, event->screenPos())) > + 15.f) { + return; + } + } else { + return; + } + } else if (event->button() == Qt::RightButton) { + if (this->isRightMouseDown) { + this->isRightMouseDown = false; + + if (fabsf(util::distanceBetweenPoints(this->lastRightPressPosition, + event->screenPos())) > 15.f) { + return; + } + } else { + return; + } + } else { + // not left or right button return; } - if (!this->isMouseDown) { - // We didn't grab the mouse press, so we shouldn't be handling the mouse - // release - return; - } - - auto app = getApp(); - - if (this->selecting) { - this->paused = false; - } - - this->isMouseDown = false; - this->selecting = false; - - float distance = util::distanceBetweenPoints(this->lastPressPosition, event->screenPos()); - - if (fabsf(distance) > 15.f) { - // It wasn't a proper click, so we don't care about that here - return; - } - - // If you clicked and released less than X pixels away, it counts - // as a click! - + // find message this->layoutMessages(); std::shared_ptr layout; QPoint relativePos; int messageIndex; + // no message found if (!tryGetMessageAt(event->pos(), layout, relativePos, messageIndex)) { // No message at clicked position this->userPopupWidget.hide(); @@ -870,8 +916,9 @@ void ChannelView::mouseReleaseEvent(QMouseEvent *event) } // message under cursor is collapsed - if (layout->getMessage()->flags & Message::MessageFlags::Collapsed) { - layout->getMessage()->flags &= ~Message::MessageFlags::Collapsed; + if (layout->flags & MessageLayout::Collapsed) { + layout->flags |= MessageLayout::Expanded; + layout->flags |= MessageLayout::RequiresLayout; this->layoutMessages(); return; @@ -883,12 +930,130 @@ void ChannelView::mouseReleaseEvent(QMouseEvent *event) return; } - auto &link = hoverLayoutElement->getLink(); - if (event->button() != Qt::LeftButton || !app->settings->linksDoubleClickOnly) { - this->handleLinkClick(event, link, layout.get()); + // handle the click + this->handleMouseClick(event, hoverLayoutElement, layout.get()); +} + +void ChannelView::handleMouseClick(QMouseEvent *event, + const messages::MessageLayoutElement *hoveredElement, + messages::MessageLayout *layout) +{ + switch (event->button()) { + case Qt::LeftButton: { + if (this->selecting) { + if (this->messagesAddedSinceSelectionPause > + SELECTION_RESUME_SCROLLING_MSG_THRESHOLD) { + this->showingLatestMessages = false; + } + + this->pausedBySelection = false; + this->selecting = false; + this->pauseTimeout.stop(); + this->pausedTemporarily = false; + + this->layoutMessages(); + } + + auto &link = hoveredElement->getLink(); + if (!getApp()->settings->linksDoubleClickOnly) { + this->handleLinkClick(event, link, layout); + + this->linkClicked.invoke(link); + } + } break; + case Qt::RightButton: { + this->addContextMenuItems(hoveredElement, layout); + } break; + default:; + } +} + +void ChannelView::addContextMenuItems(const messages::MessageLayoutElement *hoveredElement, + messages::MessageLayout *layout) +{ + const auto &creator = hoveredElement->getCreator(); + auto creatorFlags = creator.getFlags(); + + static QMenu *menu = new QMenu; + menu->clear(); + + // Emote actions + if (creatorFlags & (MessageElement::Flags::EmoteImages | MessageElement::Flags::EmojiImage)) { + const auto &emoteElement = static_cast(creator); + + // TODO: We might want to add direct "Open image" variants alongside the Copy + // actions + + if (emoteElement.data.image1x != nullptr) { + menu->addAction("Copy 1x link", [url = emoteElement.data.image1x->getUrl()] { + QApplication::clipboard()->setText(url); // + }); + } + if (emoteElement.data.image2x != nullptr) { + menu->addAction("Copy 2x link", [url = emoteElement.data.image2x->getUrl()] { + QApplication::clipboard()->setText(url); // + }); + } + if (emoteElement.data.image3x != nullptr) { + menu->addAction("Copy 3x link", [url = emoteElement.data.image3x->getUrl()] { + QApplication::clipboard()->setText(url); // + }); + } + + if ((creatorFlags & MessageElement::Flags::BttvEmote) != 0) { + menu->addSeparator(); + QString emotePageLink = emoteElement.data.pageLink; + menu->addAction("Copy BTTV emote link", [emotePageLink] { + QApplication::clipboard()->setText(emotePageLink); // + }); + } else if ((creatorFlags & MessageElement::Flags::FfzEmote) != 0) { + menu->addSeparator(); + QString emotePageLink = emoteElement.data.pageLink; + menu->addAction("Copy FFZ emote link", [emotePageLink] { + QApplication::clipboard()->setText(emotePageLink); // + }); + } } - this->linkClicked.invoke(link); + // add seperator + if (!menu->actions().empty()) { + menu->addSeparator(); + } + + // Link copy + if (hoveredElement->getLink().type == Link::Url) { + QString url = hoveredElement->getLink().value; + + menu->addAction("Open link in browser", [url] { QDesktopServices::openUrl(QUrl(url)); }); + menu->addAction("Copy link", [url] { QApplication::clipboard()->setText(url); }); + + menu->addSeparator(); + } + + // Copy actions + menu->addAction("Copy selection", + [this] { QGuiApplication::clipboard()->setText(this->getSelectedText()); }); + + if (!this->selection.isEmpty()) { + menu->addAction("Copy message", [layout] { + QString copyString; + layout->addSelectionText(copyString); + + QGuiApplication::clipboard()->setText(copyString); + }); + } + + // menu->addAction("Quote message", [layout] { + // QString copyString; + // layout->addSelectionText(copyString); + + // // insert into input + // }); + + menu->move(QCursor::pos()); + menu->show(); + + return; } void ChannelView::mouseDoubleClickEvent(QMouseEvent *event) @@ -905,7 +1070,7 @@ void ChannelView::mouseDoubleClickEvent(QMouseEvent *event) } // message under cursor is collapsed - if (layout->getMessage()->flags & Message::Collapsed) { + if (layout->flags & MessageLayout::Collapsed) { return; } @@ -933,6 +1098,10 @@ void ChannelView::hideEvent(QHideEvent *) void ChannelView::handleLinkClick(QMouseEvent *event, const messages::Link &link, messages::MessageLayout *layout) { + if (event->button() != Qt::LeftButton) { + return; + } + switch (link.type) { case messages::Link::UserInfo: { auto user = link.value; @@ -945,24 +1114,7 @@ void ChannelView::handleLinkClick(QMouseEvent *event, const messages::Link &link break; } case messages::Link::Url: { - if (event->button() == Qt::RightButton) { - static QMenu *menu = nullptr; - static QString url; - - if (menu == nullptr) { - menu = new QMenu; - menu->addAction("Open in browser", - [] { QDesktopServices::openUrl(QUrl(url)); }); - menu->addAction("Copy to clipboard", - [] { QApplication::clipboard()->setText(url); }); - } - - url = link.value; - menu->move(QCursor::pos()); - menu->show(); - } else { - QDesktopServices::openUrl(QUrl(link.value)); - } + QDesktopServices::openUrl(QUrl(link.value)); break; } case messages::Link::UserAction: { diff --git a/src/widgets/helper/channelview.hpp b/src/widgets/helper/channelview.hpp index 47262632..db74aa2b 100644 --- a/src/widgets/helper/channelview.hpp +++ b/src/widgets/helper/channelview.hpp @@ -86,7 +86,11 @@ private: bool updateQueued = false; bool messageWasAdded = false; bool lastMessageHasAlternateBackground = false; - bool paused = false; + + bool pausedTemporarily = false; + bool pausedBySelection = false; + int messagesAddedSinceSelectionPause = 0; + QTimer pauseTimeout; boost::optional overrideFlags; messages::MessageLayoutPtr lastReadMessage; @@ -99,6 +103,16 @@ private: void drawMessages(QPainter &painter); void setSelection(const messages::SelectionItem &start, const messages::SelectionItem &end); messages::MessageElement::Flags getFlags() const; + bool isPaused(); + + void handleMouseClick(QMouseEvent *event, + const messages::MessageLayoutElement *hoverLayoutElement, + messages::MessageLayout *layout); + void addContextMenuItems(const messages::MessageLayoutElement *hoveredElement, + messages::MessageLayout *layout); + + // void beginPause(); + // void endPause(); ChannelPtr channel; @@ -115,7 +129,9 @@ private: // Mouse event variables bool isMouseDown = false; + bool isRightMouseDown = false; QPointF lastPressPosition; + QPointF lastRightPressPosition; messages::Selection selection; bool selecting = false; diff --git a/src/widgets/helper/dropoverlay.cpp b/src/widgets/helper/dropoverlay.cpp new file mode 100644 index 00000000..f98160a5 --- /dev/null +++ b/src/widgets/helper/dropoverlay.cpp @@ -0,0 +1,13 @@ +//#include "dropoverlay.hpp" + +// namespace chatterino { +// namespace widgets { +// namespace helper { + +// DropOverlay::DropOverlay() +//{ +//} + +//} // namespace helper +//} // namespace widgets +//} // namespace chatterino diff --git a/src/widgets/helper/dropoverlay.hpp b/src/widgets/helper/dropoverlay.hpp new file mode 100644 index 00000000..7ae89104 --- /dev/null +++ b/src/widgets/helper/dropoverlay.hpp @@ -0,0 +1,11 @@ +//#pragma once + +//#include "widgets/helper/splitnode.hpp" + +// namespace chatterino { +// namespace widgets { +// namespace helper { + +//} // namespace helper +//} // namespace widgets +//} // namespace chatterino diff --git a/src/widgets/helper/label.hpp b/src/widgets/helper/label.hpp index 5afa0e00..3af4eadf 100644 --- a/src/widgets/helper/label.hpp +++ b/src/widgets/helper/label.hpp @@ -27,7 +27,7 @@ protected: private: QSize preferedSize; QString text; - FontStyle fontStyle = FontStyle::Medium; + FontStyle fontStyle = FontStyle::ChatMedium; }; } // namespace widgets diff --git a/src/widgets/helper/notebookbutton.cpp b/src/widgets/helper/notebookbutton.cpp index 48d97716..5a3b651b 100644 --- a/src/widgets/helper/notebookbutton.cpp +++ b/src/widgets/helper/notebookbutton.cpp @@ -45,12 +45,20 @@ void NotebookButton::paintEvent(QPaintEvent *) float h = height(), w = width(); if (icon == IconPlus) { - painter.fillRect( - QRectF((h / 12) * 2 + 1, (h / 12) * 5 + 1, w - ((h / 12) * 5), (h / 12) * 1), - foreground); - painter.fillRect( - QRectF((h / 12) * 5 + 1, (h / 12) * 2 + 1, (h / 12) * 1, w - ((h / 12) * 5)), - foreground); + painter.setPen([&] { + QColor tmp = foreground; + if (!this->mouseOver) { + tmp.setAlpha(180); + } + return tmp; + }()); + QRect rect = this->rect(); + int s = h * 4 / 9; + + painter.drawLine(rect.left() + rect.width() / 2 - (s / 2), rect.top() + rect.height() / 2, + rect.left() + rect.width() / 2 + (s / 2), rect.top() + rect.height() / 2); + painter.drawLine(rect.left() + rect.width() / 2, rect.top() + rect.height() / 2 - (s / 2), + rect.left() + rect.width() / 2, rect.top() + rect.height() / 2 + (s / 2)); } else if (icon == IconUser) { painter.setRenderHint(QPainter::Antialiasing); painter.setRenderHint(QPainter::HighQualityAntialiasing); @@ -139,10 +147,12 @@ void NotebookButton::dropEvent(QDropEvent *event) Notebook *notebook = dynamic_cast(this->parentWidget()); if (notebook != nuuls) { - SplitContainer *tab = notebook->addNewPage(); + SplitContainer *page = new SplitContainer(notebook); + auto *tab = notebook->addPage(page); + page->setTab(tab); - SplitContainer::draggingSplit->setParent(tab); - tab->addToLayout(SplitContainer::draggingSplit); + SplitContainer::draggingSplit->setParent(page); + page->appendSplit(SplitContainer::draggingSplit); } } } diff --git a/src/widgets/helper/notebooktab.cpp b/src/widgets/helper/notebooktab.cpp index 20774bfd..3c89639c 100644 --- a/src/widgets/helper/notebooktab.cpp +++ b/src/widgets/helper/notebooktab.cpp @@ -13,441 +13,55 @@ #include #include #include +#include #include #include namespace chatterino { namespace widgets { -NotebookTab2::NotebookTab2(Notebook2 *_notebook) - : BaseWidget(_notebook) - , positionChangedAnimation(this, "pos") - , notebook(_notebook) - , menu(this) +NotebookTab::NotebookTab(Notebook *notebook) + : BaseWidget(notebook) + , positionChangedAnimation_(this, "pos") + , notebook_(notebook) + , menu_(this) { auto app = getApp(); this->setAcceptDrops(true); - this->positionChangedAnimation.setEasingCurve(QEasingCurve(QEasingCurve::InCubic)); - - app->settings->showTabCloseButton.connect(boost::bind(&NotebookTab2::hideTabXChanged, this, _1), - this->managedConnections); - - this->setMouseTracking(true); - - this->menu.addAction("Rename", [this]() { - TextInputDialog d(this); - - d.setWindowTitle("Change tab title (Leave empty for default behaviour)"); - if (this->useDefaultTitle) { - d.setText(""); - } else { - d.setText(this->getTitle()); - d.highlightText(); - } - - if (d.exec() == QDialog::Accepted) { - QString newTitle = d.getText(); - if (newTitle.isEmpty()) { - this->useDefaultTitle = true; - - // fourtf: xD - // this->page->refreshTitle(); - } else { - this->useDefaultTitle = false; - this->setTitle(newTitle); - } - } - }); - - QAction *enableHighlightsOnNewMessageAction = - new QAction("Enable highlights on new message", &this->menu); - enableHighlightsOnNewMessageAction->setCheckable(true); - - this->menu.addAction("Close", [=]() { this->notebook->removePage(this->page); }); - - this->menu.addAction(enableHighlightsOnNewMessageAction); - - QObject::connect(enableHighlightsOnNewMessageAction, &QAction::toggled, [this](bool newValue) { - debug::Log("New value is {}", newValue); // - }); -} - -void NotebookTab2::themeRefreshEvent() -{ - this->update(); -} - -void NotebookTab2::updateSize() -{ - auto app = getApp(); - float scale = getScale(); - - int width; - QFontMetrics metrics(this->font()); - - if (!app->settings->showTabCloseButton) { - width = (int)((metrics.width(this->title) + 16 /*+ 16*/) * scale); - } else { - width = (int)((metrics.width(this->title) + 8 + 24 /*+ 16*/) * scale); - } - - this->resize(std::min((int)(150 * scale), width), (int)(24 * scale)); - - // if (this->parent() != nullptr) { - // (static_cast(this->parent()))->performLayout(true); - // } -} - -const QString &NotebookTab2::getTitle() const -{ - return this->title; -} - -void NotebookTab2::setTitle(const QString &newTitle) -{ - if (this->title != newTitle) { - this->title = newTitle; - this->updateSize(); - this->update(); - } -} - -bool NotebookTab2::isSelected() const -{ - return this->selected; -} - -void NotebookTab2::setSelected(bool value) -{ - this->selected = value; - - this->highlightState = HighlightState::None; - - this->update(); -} - -void NotebookTab2::setHighlightState(HighlightState newHighlightStyle) -{ - if (this->isSelected()) { - return; - } - - if (this->highlightState != HighlightState::Highlighted) { - this->highlightState = newHighlightStyle; - - this->update(); - } -} - -QRect NotebookTab2::getDesiredRect() const -{ - return QRect(positionAnimationDesiredPoint, size()); -} - -void NotebookTab2::hideTabXChanged(bool) -{ - this->updateSize(); - this->update(); -} - -void NotebookTab2::moveAnimated(QPoint pos, bool animated) -{ - this->positionAnimationDesiredPoint = pos; - - QWidget *w = this->window(); - - if ((w != nullptr && !w->isVisible()) || !animated || !positionChangedAnimationRunning) { - this->move(pos); - - this->positionChangedAnimationRunning = true; - return; - } - - if (this->positionChangedAnimation.endValue() == pos) { - return; - } - - this->positionChangedAnimation.stop(); - this->positionChangedAnimation.setDuration(75); - this->positionChangedAnimation.setStartValue(this->pos()); - this->positionChangedAnimation.setEndValue(pos); - this->positionChangedAnimation.start(); -} - -void NotebookTab2::paintEvent(QPaintEvent *) -{ - auto app = getApp(); - QPainter painter(this); - float scale = this->getScale(); - - int height = (int)(scale * 24); - // int fullHeight = (int)(scale * 48); - - // select the right tab colors - singletons::ThemeManager::TabColors colors; - singletons::ThemeManager::TabColors regular = this->themeManager->tabs.regular; - - if (this->selected) { - colors = this->themeManager->tabs.selected; - } else if (this->highlightState == HighlightState::Highlighted) { - colors = this->themeManager->tabs.highlighted; - } else if (this->highlightState == HighlightState::NewMessage) { - colors = this->themeManager->tabs.newMessage; - } else { - colors = this->themeManager->tabs.regular; - } - - bool windowFocused = this->window() == QApplication::activeWindow(); - // || SettingsDialog::getHandle() == QApplication::activeWindow(); - - QBrush tabBackground = this->mouseOver ? colors.backgrounds.hover - : (windowFocused ? colors.backgrounds.regular - : colors.backgrounds.unfocused); - - if (true) { - painter.fillRect(rect(), this->mouseOver ? regular.backgrounds.hover - : (windowFocused ? regular.backgrounds.regular - : regular.backgrounds.unfocused)); - - // fill the tab background - painter.fillRect(rect(), tabBackground); - - // draw border - // painter.setPen(QPen("#ccc")); - // QPainterPath path(QPointF(0, height)); - // path.lineTo(0, 0); - // path.lineTo(this->width() - 1, 0); - // path.lineTo(this->width() - 1, this->height() - 1); - // path.lineTo(0, this->height() - 1); - // painter.drawPath(path); - } else { - // QPainterPath path(QPointF(0, height)); - // path.lineTo(8 * scale, 0); - // path.lineTo(this->width() - 8 * scale, 0); - // path.lineTo(this->width(), height); - // painter.fillPath(path, this->mouseOver ? regular.backgrounds.hover - // : (windowFocused ? - // regular.backgrounds.regular - // : - // regular.backgrounds.unfocused)); - - // // fill the tab background - // painter.fillPath(path, tabBackground); - // painter.setPen(QColor("#FFF")); - // painter.setRenderHint(QPainter::Antialiasing); - // painter.drawPath(path); - // // painter.setBrush(QColor("#000")); - - // QLinearGradient gradient(0, height, 0, fullHeight); - // gradient.setColorAt(0, tabBackground.color()); - // gradient.setColorAt(1, "#fff"); - - // QBrush brush(gradient); - // painter.fillRect(0, height, this->width(), fullHeight - height, - // brush); - } - - // set the pen color - painter.setPen(colors.text); - - // set area for text - int rectW = (!app->settings->showTabCloseButton ? 0 : static_cast(16) * scale); - QRect rect(0, 0, this->width() - rectW, height); - - // draw text - if (true) { // legacy - // painter.drawText(rect, this->getTitle(), QTextOption(Qt::AlignCenter)); - int offset = (int)(scale * 8); - QRect textRect(offset, 0, this->width() - offset - offset, height); - - QTextOption option(Qt::AlignLeft | Qt::AlignVCenter); - option.setWrapMode(QTextOption::NoWrap); - painter.drawText(textRect, this->getTitle(), option); - } else { - // QTextOption option(Qt::AlignLeft | Qt::AlignVCenter); - // option.setWrapMode(QTextOption::NoWrap); - // int offset = (int)(scale * 16); - // QRect textRect(offset, 0, this->width() - offset - offset, height); - // painter.drawText(textRect, this->getTitle(), option); - } - - // draw close x - if (!app->settings->showTabCloseButton && (mouseOver || selected)) { - QRect xRect = this->getXRect(); - if (!xRect.isNull()) { - if (mouseOverX) { - painter.fillRect(xRect, QColor(0, 0, 0, 64)); - - if (mouseDownX) { - painter.fillRect(xRect, QColor(0, 0, 0, 64)); - } - } - - int a = static_cast(scale * 4); - - painter.drawLine(xRect.topLeft() + QPoint(a, a), xRect.bottomRight() + QPoint(-a, -a)); - painter.drawLine(xRect.topRight() + QPoint(-a, a), xRect.bottomLeft() + QPoint(a, -a)); - } - } -} - -void NotebookTab2::mousePressEvent(QMouseEvent *event) -{ - this->mouseDown = true; - this->mouseDownX = this->getXRect().contains(event->pos()); - - this->update(); - - this->notebook->select(page); - - if (this->notebook->getAllowUserTabManagement()) { - switch (event->button()) { - case Qt::RightButton: { - this->menu.popup(event->globalPos()); - } break; - } - } -} - -void NotebookTab2::mouseReleaseEvent(QMouseEvent *event) -{ - this->mouseDown = false; - - if (event->button() == Qt::MiddleButton) { - if (this->rect().contains(event->pos())) { - this->notebook->removePage(this->page); - } - } else { - if (getApp()->settings->showTabCloseButton && this->mouseDownX && - this->getXRect().contains(event->pos())) { - this->mouseDownX = false; - - this->notebook->removePage(this->page); - } else { - this->update(); - } - } -} - -void NotebookTab2::enterEvent(QEvent *) -{ - this->mouseOver = true; - - this->update(); -} - -void NotebookTab2::leaveEvent(QEvent *) -{ - this->mouseOverX = false; - this->mouseOver = false; - - this->update(); -} - -void NotebookTab2::dragEnterEvent(QDragEnterEvent *) -{ - if (this->notebook->getAllowUserTabManagement()) { - this->notebook->select(this->page); - } -} - -void NotebookTab2::mouseMoveEvent(QMouseEvent *event) -{ - auto app = getApp(); - - if (app->settings->showTabCloseButton && this->notebook->getAllowUserTabManagement()) // - { - bool overX = this->getXRect().contains(event->pos()); - - if (overX != this->mouseOverX) { - // Over X state has been changed (we either left or entered it; - this->mouseOverX = overX; - - this->update(); - } - } - - QPoint relPoint = this->mapToParent(event->pos()); - - if (this->mouseDown && !this->getDesiredRect().contains(relPoint) && - this->notebook->getAllowUserTabManagement()) // - { - int index; - QWidget *clickedPage = notebook->tabAt(relPoint, index, this->width()); - - assert(clickedPage); - - if (clickedPage != nullptr && clickedPage != this->page) { - this->notebook->rearrangePage(this->page, index); - } - } -} - -QRect NotebookTab2::getXRect() -{ - if (this->notebook->getAllowUserTabManagement()) { - return QRect(); - } - - float s = this->getScale(); - return QRect(this->width() - static_cast(20 * s), static_cast(4 * s), - static_cast(16 * s), static_cast(16 * s)); -} - -// 2 -NotebookTab::NotebookTab(Notebook *_notebook) - : BaseWidget(_notebook) - , positionChangedAnimation(this, "pos") - , notebook(_notebook) - , menu(this) -{ - auto app = getApp(); - - this->setAcceptDrops(true); - - this->positionChangedAnimation.setEasingCurve(QEasingCurve(QEasingCurve::InCubic)); + this->positionChangedAnimation_.setEasingCurve(QEasingCurve(QEasingCurve::InCubic)); app->settings->showTabCloseButton.connect(boost::bind(&NotebookTab::hideTabXChanged, this, _1), - this->managedConnections); + this->managedConnections_); this->setMouseTracking(true); - this->menu.addAction("Rename", [this]() { + this->menu_.addAction("Rename", [this]() { TextInputDialog d(this); d.setWindowTitle("Change tab title (Leave empty for default behaviour)"); - if (this->useDefaultTitle) { - d.setText(""); - } else { - d.setText(this->getTitle()); - d.highlightText(); - } + d.setText(this->getCustomTitle()); + d.highlightText(); if (d.exec() == QDialog::Accepted) { QString newTitle = d.getText(); - if (newTitle.isEmpty()) { - this->useDefaultTitle = true; - this->page->refreshTitle(); - } else { - this->useDefaultTitle = false; - this->setTitle(newTitle); - } + this->setCustomTitle(newTitle); } }); - QAction *enableHighlightsOnNewMessageAction = - new QAction("Enable highlights on new message", &this->menu); - enableHighlightsOnNewMessageAction->setCheckable(true); + // QAction *enableHighlightsOnNewMessageAction = + // new QAction("Enable highlights on new message", &this->menu); + // enableHighlightsOnNewMessageAction->setCheckable(true); - this->menu.addAction("Close", [=]() { this->notebook->removePage(this->page); }); + this->menu_.addAction("Close", [=]() { this->notebook_->removePage(this->page); }); - this->menu.addAction(enableHighlightsOnNewMessageAction); + // this->menu.addAction(enableHighlightsOnNewMessageAction); - QObject::connect(enableHighlightsOnNewMessageAction, &QAction::toggled, [](bool newValue) { - debug::Log("New value is {}", newValue); // - }); + // QObject::connect(enableHighlightsOnNewMessageAction, &QAction::toggled, [this](bool + // newValue) { + // debug::Log("New value is {}", newValue); // + // }); } void NotebookTab::themeRefreshEvent() @@ -457,49 +71,86 @@ void NotebookTab::themeRefreshEvent() void NotebookTab::updateSize() { - auto app = getApp(); - float scale = getScale(); int width; + QFontMetrics metrics = getApp()->fonts->getFontMetrics( + FontStyle::UiTabs, float(qreal(this->getScale()) * this->devicePixelRatioF())); - if (!app->settings->showTabCloseButton) { - width = (int)((fontMetrics().width(this->title) + 16 /*+ 16*/) * scale); + if (this->hasXButton()) { + width = int((metrics.width(this->getTitle()) + 32) * scale); } else { - width = (int)((fontMetrics().width(this->title) + 8 + 24 /*+ 16*/) * scale); + width = int((metrics.width(this->getTitle()) + 16) * scale); } - this->resize(std::min((int)(150 * scale), width), (int)(24 * scale)); + width = std::max(this->height(), std::min(int(150 * scale), width)); - if (this->parent() != nullptr) { - (static_cast(this->parent()))->performLayout(true); + if (this->width() != width) { + this->resize(width, int(NOTEBOOK_TAB_HEIGHT * scale)); + this->notebook_->performLayout(); } } +const QString &NotebookTab::getCustomTitle() const +{ + return this->customTitle_; +} + +void NotebookTab::setCustomTitle(const QString &newTitle) +{ + if (this->customTitle_ != newTitle) { + this->customTitle_ = newTitle; + this->titleUpdated(); + } +} + +void NotebookTab::resetCustomTitle() +{ + this->setCustomTitle(QString()); +} + +bool NotebookTab::hasCustomTitle() const +{ + return !this->customTitle_.isEmpty(); +} + +void NotebookTab::setDefaultTitle(const QString &title) +{ + if (this->defaultTitle_ != title) { + this->defaultTitle_ = title; + + if (this->customTitle_.isEmpty()) { + this->titleUpdated(); + } + } +} + +const QString &NotebookTab::getDefaultTitle() const +{ + return this->defaultTitle_; +} + const QString &NotebookTab::getTitle() const { - return this->title; + return this->customTitle_.isEmpty() ? this->defaultTitle_ : this->customTitle_; } -void NotebookTab::setTitle(const QString &newTitle) +void NotebookTab::titleUpdated() { - if (this->title != newTitle) { - this->title = newTitle; - this->updateSize(); - this->update(); - } + this->updateSize(); + this->update(); } bool NotebookTab::isSelected() const { - return this->selected; + return this->selected_; } void NotebookTab::setSelected(bool value) { - this->selected = value; + this->selected_ = value; - this->highlightState = HighlightState::None; + this->highlightState_ = HighlightState::None; this->update(); } @@ -510,8 +161,8 @@ void NotebookTab::setHighlightState(HighlightState newHighlightStyle) return; } - if (this->highlightState != HighlightState::Highlighted) { - this->highlightState = newHighlightStyle; + if (this->highlightState_ != HighlightState::Highlighted) { + this->highlightState_ = newHighlightStyle; this->update(); } @@ -519,7 +170,7 @@ void NotebookTab::setHighlightState(HighlightState newHighlightStyle) QRect NotebookTab::getDesiredRect() const { - return QRect(positionAnimationDesiredPoint, size()); + return QRect(this->positionAnimationDesiredPoint_, size()); } void NotebookTab::hideTabXChanged(bool) @@ -530,26 +181,26 @@ void NotebookTab::hideTabXChanged(bool) void NotebookTab::moveAnimated(QPoint pos, bool animated) { - this->positionAnimationDesiredPoint = pos; + this->positionAnimationDesiredPoint_ = pos; QWidget *w = this->window(); - if ((w != nullptr && !w->isVisible()) || !animated || !positionChangedAnimationRunning) { + if ((w != nullptr && !w->isVisible()) || !animated || !this->positionChangedAnimationRunning_) { this->move(pos); - this->positionChangedAnimationRunning = true; + this->positionChangedAnimationRunning_ = true; return; } - if (this->positionChangedAnimation.endValue() == pos) { + if (this->positionChangedAnimation_.endValue() == pos) { return; } - this->positionChangedAnimation.stop(); - this->positionChangedAnimation.setDuration(75); - this->positionChangedAnimation.setStartValue(this->pos()); - this->positionChangedAnimation.setEndValue(pos); - this->positionChangedAnimation.start(); + this->positionChangedAnimation_.stop(); + this->positionChangedAnimation_.setDuration(75); + this->positionChangedAnimation_.setStartValue(this->pos()); + this->positionChangedAnimation_.setEndValue(pos); + this->positionChangedAnimation_.start(); } void NotebookTab::paintEvent(QPaintEvent *) @@ -558,18 +209,22 @@ void NotebookTab::paintEvent(QPaintEvent *) QPainter painter(this); float scale = this->getScale(); - int height = (int)(scale * 24); + painter.setFont(getApp()->fonts->getFont(FontStyle::UiTabs, scale * this->devicePixelRatioF())); + QFontMetrics metrics = + app->fonts->getFontMetrics(FontStyle::UiTabs, scale * this->devicePixelRatioF()); + + int height = int(scale * NOTEBOOK_TAB_HEIGHT); // int fullHeight = (int)(scale * 48); // select the right tab colors singletons::ThemeManager::TabColors colors; singletons::ThemeManager::TabColors regular = this->themeManager->tabs.regular; - if (this->selected) { + if (this->selected_) { colors = this->themeManager->tabs.selected; - } else if (this->highlightState == HighlightState::Highlighted) { + } else if (this->highlightState_ == HighlightState::Highlighted) { colors = this->themeManager->tabs.highlighted; - } else if (this->highlightState == HighlightState::NewMessage) { + } else if (this->highlightState_ == HighlightState::NewMessage) { colors = this->themeManager->tabs.newMessage; } else { colors = this->themeManager->tabs.regular; @@ -578,127 +233,139 @@ void NotebookTab::paintEvent(QPaintEvent *) bool windowFocused = this->window() == QApplication::activeWindow(); // || SettingsDialog::getHandle() == QApplication::activeWindow(); - QBrush tabBackground = this->mouseOver ? colors.backgrounds.hover - : (windowFocused ? colors.backgrounds.regular - : colors.backgrounds.unfocused); + QBrush tabBackground = this->mouseOver_ ? colors.backgrounds.hover + : (windowFocused ? colors.backgrounds.regular + : colors.backgrounds.unfocused); - if (true) { - painter.fillRect(rect(), this->mouseOver ? regular.backgrounds.hover - : (windowFocused ? regular.backgrounds.regular - : regular.backgrounds.unfocused)); + // painter.fillRect(rect(), this->mouseOver_ ? regular.backgrounds.hover + // : (windowFocused ? regular.backgrounds.regular + // : + // regular.backgrounds.unfocused)); - // fill the tab background - painter.fillRect(rect(), tabBackground); + // fill the tab background + auto bgRect = rect(); + bgRect.setTop(bgRect.top() + 2); - // draw border - // painter.setPen(QPen("#ccc")); - // QPainterPath path(QPointF(0, height)); - // path.lineTo(0, 0); - // path.lineTo(this->width() - 1, 0); - // path.lineTo(this->width() - 1, this->height() - 1); - // path.lineTo(0, this->height() - 1); - // painter.drawPath(path); - } else { - // QPainterPath path(QPointF(0, height)); - // path.lineTo(8 * scale, 0); - // path.lineTo(this->width() - 8 * scale, 0); - // path.lineTo(this->width(), height); - // painter.fillPath(path, this->mouseOver ? regular.backgrounds.hover - // : (windowFocused ? - // regular.backgrounds.regular - // : - // regular.backgrounds.unfocused)); + painter.fillRect(bgRect, tabBackground); - // // fill the tab background - // painter.fillPath(path, tabBackground); - // painter.setPen(QColor("#FFF")); - // painter.setRenderHint(QPainter::Antialiasing); - // painter.drawPath(path); - // // painter.setBrush(QColor("#000")); + // draw border + // painter.setPen(QPen("#fff")); + // QPainterPath path(QPointF(0, height)); + // path.lineTo(0, 0); + // path.lineTo(this->width() - 1, 0); + // path.lineTo(this->width() - 1, this->height() - 1); + // path.lineTo(0, this->height() - 1); + // painter.drawPath(path); - // QLinearGradient gradient(0, height, 0, fullHeight); - // gradient.setColorAt(0, tabBackground.color()); - // gradient.setColorAt(1, "#fff"); - - // QBrush brush(gradient); - // painter.fillRect(0, height, this->width(), fullHeight - height, - // brush); - } + // top line + painter.fillRect(QRectF(0, (this->selected_ ? 0.f : 1.f) * scale, this->width(), + (this->selected_ ? 2.f : 1.f) * scale), + this->mouseOver_ + ? colors.line.hover + : (windowFocused ? colors.line.regular : colors.line.unfocused)); // set the pen color painter.setPen(colors.text); // set area for text - int rectW = (!app->settings->showTabCloseButton ? 0 : static_cast(16) * scale); + int rectW = (!app->settings->showTabCloseButton ? 0 : int(16 * scale)); QRect rect(0, 0, this->width() - rectW, height); // draw text - if (true) { // legacy - // painter.drawText(rect, this->getTitle(), QTextOption(Qt::AlignCenter)); - int offset = (int)(scale * 8); - QRect textRect(offset, 0, this->width() - offset - offset, height); + int offset = int(scale * 8); + QRect textRect(offset, this->selected_ ? 1 : 2, this->width() - offset - offset, height); - QTextOption option(Qt::AlignLeft | Qt::AlignVCenter); - option.setWrapMode(QTextOption::NoWrap); - painter.drawText(textRect, this->getTitle(), option); - } else { - // QTextOption option(Qt::AlignLeft | Qt::AlignVCenter); - // option.setWrapMode(QTextOption::NoWrap); - // int offset = (int)(scale * 16); - // QRect textRect(offset, 0, this->width() - offset - offset, height); - // painter.drawText(textRect, this->getTitle(), option); + if (this->shouldDrawXButton()) { + textRect.setRight(textRect.right() - this->height() / 2); } + int width = metrics.width(this->getTitle()); + Qt::Alignment alignment = width > textRect.width() ? Qt::AlignLeft | Qt::AlignVCenter + : Qt::AlignHCenter | Qt::AlignVCenter; + + QTextOption option(alignment); + option.setWrapMode(QTextOption::NoWrap); + painter.drawText(textRect, this->getTitle(), option); + // draw close x - if (app->settings->showTabCloseButton && (mouseOver || selected)) { + if (this->shouldDrawXButton()) { QRect xRect = this->getXRect(); - if (mouseOverX) { - painter.fillRect(xRect, QColor(0, 0, 0, 64)); + if (!xRect.isNull()) { + painter.setBrush(QColor("#fff")); - if (mouseDownX) { + if (this->mouseOverX_) { painter.fillRect(xRect, QColor(0, 0, 0, 64)); + + if (this->mouseDownX_) { + painter.fillRect(xRect, QColor(0, 0, 0, 64)); + } } + + int a = static_cast(scale * 4); + + painter.drawLine(xRect.topLeft() + QPoint(a, a), xRect.bottomRight() + QPoint(-a, -a)); + painter.drawLine(xRect.topRight() + QPoint(-a, a), xRect.bottomLeft() + QPoint(a, -a)); } - - int a = static_cast(scale * 4); - - painter.drawLine(xRect.topLeft() + QPoint(a, a), xRect.bottomRight() + QPoint(-a, -a)); - painter.drawLine(xRect.topRight() + QPoint(-a, a), xRect.bottomLeft() + QPoint(a, -a)); } -} // namespace widgets + + // draw line at bottom + if (!this->selected_) { + painter.fillRect(0, this->height() - 1, this->width(), 1, app->themes->window.background); + } +} + +bool NotebookTab::hasXButton() +{ + return getApp()->settings->showTabCloseButton && this->notebook_->getAllowUserTabManagement(); +} + +bool NotebookTab::shouldDrawXButton() +{ + return this->hasXButton() && (this->mouseOver_ || this->selected_); +} void NotebookTab::mousePressEvent(QMouseEvent *event) { - this->mouseDown = true; - this->mouseDownX = this->getXRect().contains(event->pos()); + this->mouseDown_ = true; + this->mouseDownX_ = this->getXRect().contains(event->pos()); this->update(); - this->notebook->select(page); + this->notebook_->select(page); - switch (event->button()) { - case Qt::RightButton: { - this->menu.popup(event->globalPos()); - } break; + if (this->notebook_->getAllowUserTabManagement()) { + switch (event->button()) { + case Qt::RightButton: { + this->menu_.popup(event->globalPos()); + } break; + default:; + } } } void NotebookTab::mouseReleaseEvent(QMouseEvent *event) { - auto app = getApp(); + this->mouseDown_ = false; - this->mouseDown = false; + auto removeThisPage = [this] { + auto reply = QMessageBox::question(this, "Remove this tab", + "Are you sure that you want to remove this tab?", + QMessageBox::Yes | QMessageBox::Cancel); + + if (reply == QMessageBox::Yes) { + this->notebook_->removePage(this->page); + } + }; if (event->button() == Qt::MiddleButton) { if (this->rect().contains(event->pos())) { - this->notebook->removePage(this->page); + removeThisPage(); } } else { - if (app->settings->showTabCloseButton && this->mouseDownX && - this->getXRect().contains(event->pos())) { - this->mouseDownX = false; + if (this->hasXButton() && this->mouseDownX_ && this->getXRect().contains(event->pos())) { + this->mouseDownX_ = false; - this->notebook->removePage(this->page); + removeThisPage(); } else { this->update(); } @@ -707,34 +374,43 @@ void NotebookTab::mouseReleaseEvent(QMouseEvent *event) void NotebookTab::enterEvent(QEvent *) { - this->mouseOver = true; + this->mouseOver_ = true; this->update(); } void NotebookTab::leaveEvent(QEvent *) { - this->mouseOverX = false; - this->mouseOver = false; + this->mouseOverX_ = false; + this->mouseOver_ = false; this->update(); } -void NotebookTab::dragEnterEvent(QDragEnterEvent *) +void NotebookTab::dragEnterEvent(QDragEnterEvent *event) { - this->notebook->select(this->page); + if (!event->mimeData()->hasFormat("chatterino/split")) + return; + + if (!SplitContainer::isDraggingSplit) + return; + + if (this->notebook_->getAllowUserTabManagement()) { + this->notebook_->select(this->page); + } } void NotebookTab::mouseMoveEvent(QMouseEvent *event) { auto app = getApp(); - if (app->settings->showTabCloseButton) { + if (app->settings->showTabCloseButton && this->notebook_->getAllowUserTabManagement()) // + { bool overX = this->getXRect().contains(event->pos()); - if (overX != this->mouseOverX) { + if (overX != this->mouseOverX_) { // Over X state has been changed (we either left or entered it; - this->mouseOverX = overX; + this->mouseOverX_ = overX; this->update(); } @@ -742,15 +418,28 @@ void NotebookTab::mouseMoveEvent(QMouseEvent *event) QPoint relPoint = this->mapToParent(event->pos()); - if (this->mouseDown && !this->getDesiredRect().contains(relPoint)) { + if (this->mouseDown_ && !this->getDesiredRect().contains(relPoint) && + this->notebook_->getAllowUserTabManagement()) // + { int index; - SplitContainer *clickedPage = notebook->tabAt(relPoint, index, this->width()); + QWidget *clickedPage = this->notebook_->tabAt(relPoint, index, this->width()); if (clickedPage != nullptr && clickedPage != this->page) { - this->notebook->rearrangePage(this->page, index); + this->notebook_->rearrangePage(this->page, index); } } } +QRect NotebookTab::getXRect() +{ + // if (!this->notebook->getAllowUserTabManagement()) { + // return QRect(); + // } + + float s = this->getScale(); + return QRect(this->width() - static_cast(20 * s), static_cast(6 * s), + static_cast(16 * s), static_cast(16 * s)); +} + } // namespace widgets } // namespace chatterino diff --git a/src/widgets/helper/notebooktab.hpp b/src/widgets/helper/notebooktab.hpp index 628b835b..93cb971f 100644 --- a/src/widgets/helper/notebooktab.hpp +++ b/src/widgets/helper/notebooktab.hpp @@ -11,75 +11,12 @@ namespace chatterino { namespace widgets { +#define NOTEBOOK_TAB_HEIGHT 28 + +// class Notebook; class Notebook; -class Notebook2; class SplitContainer; -class NotebookTab2 : public BaseWidget -{ - Q_OBJECT - -public: - explicit NotebookTab2(Notebook2 *_notebook); - - void updateSize(); - - QWidget *page; - - const QString &getTitle() const; - void setTitle(const QString &newTitle); - bool isSelected() const; - void setSelected(bool value); - - void setHighlightState(HighlightState style); - - void moveAnimated(QPoint pos, bool animated = true); - - QRect getDesiredRect() const; - void hideTabXChanged(bool); - -protected: - virtual void themeRefreshEvent() override; - - virtual void paintEvent(QPaintEvent *) override; - - virtual void mousePressEvent(QMouseEvent *event) override; - virtual void mouseReleaseEvent(QMouseEvent *event) override; - virtual void enterEvent(QEvent *) override; - virtual void leaveEvent(QEvent *) override; - - virtual void dragEnterEvent(QDragEnterEvent *event) override; - - virtual void mouseMoveEvent(QMouseEvent *event) override; - -private: - std::vector managedConnections; - - QPropertyAnimation positionChangedAnimation; - bool positionChangedAnimationRunning = false; - QPoint positionAnimationDesiredPoint; - - Notebook2 *notebook; - - QString title; - -public: - bool useDefaultTitle = true; - -private: - bool selected = false; - bool mouseOver = false; - bool mouseDown = false; - bool mouseOverX = false; - bool mouseDownX = false; - - HighlightState highlightState = HighlightState::None; - - QMenu menu; - - QRect getXRect(); -}; - class NotebookTab : public BaseWidget { Q_OBJECT @@ -89,10 +26,16 @@ public: void updateSize(); - SplitContainer *page; + QWidget *page; + void setCustomTitle(const QString &title); + void resetCustomTitle(); + bool hasCustomTitle() const; + const QString &getCustomTitle() const; + void setDefaultTitle(const QString &title); + const QString &getDefaultTitle() const; const QString &getTitle() const; - void setTitle(const QString &newTitle); + bool isSelected() const; void setSelected(bool value); @@ -118,36 +61,32 @@ protected: virtual void mouseMoveEvent(QMouseEvent *event) override; private: - std::vector managedConnections; + std::vector managedConnections_; - QPropertyAnimation positionChangedAnimation; - bool positionChangedAnimationRunning = false; - QPoint positionAnimationDesiredPoint; + QPropertyAnimation positionChangedAnimation_; + bool positionChangedAnimationRunning_ = false; + QPoint positionAnimationDesiredPoint_; - Notebook *notebook; + Notebook *notebook_; - QString title; + QString customTitle_; + QString defaultTitle_; -public: - bool useDefaultTitle = true; + bool selected_ = false; + bool mouseOver_ = false; + bool mouseDown_ = false; + bool mouseOverX_ = false; + bool mouseDownX_ = false; -private: - bool selected = false; - bool mouseOver = false; - bool mouseDown = false; - bool mouseOverX = false; - bool mouseDownX = false; + bool hasXButton(); + bool shouldDrawXButton(); - HighlightState highlightState = HighlightState::None; + HighlightState highlightState_ = HighlightState::None; - QMenu menu; + QMenu menu_; - QRect getXRect() - { - float s = this->getScale(); - return QRect(this->width() - static_cast(20 * s), static_cast(4 * s), - static_cast(16 * s), static_cast(16 * s)); - } + QRect getXRect(); + void titleUpdated(); }; } // namespace widgets diff --git a/src/widgets/helper/resizingtextedit.cpp b/src/widgets/helper/resizingtextedit.cpp index 4b88ec29..752b1f23 100644 --- a/src/widgets/helper/resizingtextedit.cpp +++ b/src/widgets/helper/resizingtextedit.cpp @@ -137,10 +137,19 @@ void ResizingTextEdit::keyPressEvent(QKeyEvent *event) } } +void ResizingTextEdit::focusInEvent(QFocusEvent *event) +{ + QTextEdit::focusInEvent(event); + + if (event->gotFocus()) { + this->focused.invoke(); + } +} + void ResizingTextEdit::setCompleter(QCompleter *c) { if (this->completer) { - QObject::disconnect(this->completer, 0, this, 0); + QObject::disconnect(this->completer, nullptr, this, nullptr); } this->completer = c; diff --git a/src/widgets/helper/resizingtextedit.hpp b/src/widgets/helper/resizingtextedit.hpp index d68ea0eb..73661977 100644 --- a/src/widgets/helper/resizingtextedit.hpp +++ b/src/widgets/helper/resizingtextedit.hpp @@ -15,6 +15,7 @@ public: bool hasHeightForWidth() const override; pajlada::Signals::Signal keyPressed; + pajlada::Signals::NoArgSignal focused; void setCompleter(QCompleter *c); QCompleter *getCompleter() const; @@ -23,6 +24,8 @@ protected: int heightForWidth(int) const override; void keyPressEvent(QKeyEvent *event) override; + void focusInEvent(QFocusEvent *event) override; + private: QCompleter *completer = nullptr; bool completionInProgress = false; diff --git a/src/widgets/helper/scrollbarhighlight.hpp b/src/widgets/helper/scrollbarhighlight.hpp index 7dbccee1..4fb9ee93 100644 --- a/src/widgets/helper/scrollbarhighlight.hpp +++ b/src/widgets/helper/scrollbarhighlight.hpp @@ -9,7 +9,7 @@ class ScrollbarHighlight { public: enum Style : char { None, Default, Line }; - enum Color : char { Highlight }; + enum Color : char { Highlight, Subscription }; ScrollbarHighlight(); ScrollbarHighlight(Color _color, Style _style = Default); diff --git a/src/widgets/helper/splitheader.cpp b/src/widgets/helper/splitheader.cpp index 42fef1c0..8f57ca7c 100644 --- a/src/widgets/helper/splitheader.cpp +++ b/src/widgets/helper/splitheader.cpp @@ -31,7 +31,6 @@ SplitHeader::SplitHeader(Split *_split) , split(_split) { auto app = getApp(); - this->setMouseTracking(true); util::LayoutCreator layoutCreator(this); auto layout = layoutCreator.emplace().withoutMargin(); @@ -53,16 +52,26 @@ SplitHeader::SplitHeader(Split *_split) // channel name label // auto title = layout.emplace