summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/ci.yml47
-rw-r--r--CMakeLists.txt92
-rw-r--r--ci/msys-deps.txt1
-rw-r--r--cmake/Findgdk.cmake37
-rw-r--r--cmake/Findgdkmm.cmake48
-rw-r--r--cmake/Findglib.cmake98
-rw-r--r--cmake/Findgtkmm.cmake18
-rw-r--r--cmake/Findlibhandy.cmake39
-rw-r--r--res/css/application-low-priority.css25
-rw-r--r--res/res.7zbin0 -> 9870957 bytes
-rw-r--r--src/abaddon.cpp59
-rw-r--r--src/abaddon.hpp2
-rw-r--r--src/components/channels.cpp33
-rw-r--r--src/components/channels.hpp27
-rw-r--r--src/components/channeltabswitcherhandy.cpp202
-rw-r--r--src/components/channeltabswitcherhandy.hpp66
-rw-r--r--src/components/chatwindow.cpp46
-rw-r--r--src/components/chatwindow.hpp33
-rw-r--r--src/discord/channel.cpp13
-rw-r--r--src/discord/channel.hpp1
-rw-r--r--src/state.cpp11
-rw-r--r--src/state.hpp9
-rw-r--r--src/windows/mainwindow.cpp42
-rw-r--r--src/windows/mainwindow.hpp11
24 files changed, 814 insertions, 146 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ecf2224..f5ec12e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -9,6 +9,10 @@ jobs:
strategy:
matrix:
buildtype: [Debug, RelWithDebInfo, MinSizeRel]
+ mindeps: [false]
+ include:
+ - buildtype: RelWithDebInfo
+ mindeps: true
defaults:
run:
shell: msys2 {0}
@@ -17,12 +21,12 @@ jobs:
with:
submodules: true
- - name: Setup MSYS2
- uses: msys2/setup-msys2@v2
+ - name: Setup MSYS2 (1)
+ uses: haya14busa/action-cond@v1
+ id: setupmsys
with:
- msystem: mingw64
- update: true
- install: >-
+ cond: ${{ matrix.mindeps == true }}
+ if_true: >-
git
make
mingw-w64-x86_64-toolchain
@@ -33,6 +37,25 @@ jobs:
mingw-w64-x86_64-curl
mingw-w64-x86_64-zlib
mingw-w64-x86_64-gtkmm3
+ if_false: >-
+ git
+ make
+ mingw-w64-x86_64-toolchain
+ mingw-w64-x86_64-cmake
+ mingw-w64-x86_64-ninja
+ mingw-w64-x86_64-sqlite3
+ mingw-w64-x86_64-nlohmann-json
+ mingw-w64-x86_64-curl
+ mingw-w64-x86_64-zlib
+ mingw-w64-x86_64-gtkmm3
+ mingw-w64-x86_64-libhandy
+
+ - name: Setup MSYS2 (2)
+ uses: msys2/setup-msys2@v2
+ with:
+ msystem: mingw64
+ update: true
+ install: ${{ steps.setupmsys.outputs.value }}
- name: Build
run: |
@@ -49,12 +72,20 @@ jobs:
cp -r /mingw64/lib/gdk-pixbuf-2.0 build/artifactdir/lib
cp -r res/css res/res res/fonts build/artifactdir/bin
cp /mingw64/share/glib-2.0/schemas/gschemas.compiled build/artifactdir/share/glib-2.0/schemas
- cat "ci/msys-deps.txt" | sed 's/\r$//' | xargs -I % cp /mingw64% build/artifactdir/bin
+ cat "ci/msys-deps.txt" | sed 's/\r$//' | xargs -I % cp /mingw64% build/artifactdir/bin || :
- - name: Upload build
+ - name: Upload build (1)
+ uses: haya14busa/action-cond@v1
+ id: buildname
+ with:
+ cond: ${{ matrix.mindeps == true }}
+ if_true: "${{ matrix.buildtype }}-mindeps"
+ if_false: "${{ matrix.buildtype }}"
+
+ - name: Upload build (2)
uses: actions/upload-artifact@v2
with:
- name: build-windows-msys2-${{ matrix.buildtype }}
+ name: build-windows-msys2-${{ steps.buildname.outputs.value }}
path: build/artifactdir
mac:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index fa56d6c..ab6eb5b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -7,6 +7,8 @@ set(ABADDON_RESOURCE_DIR "/usr/share/abaddon" CACHE PATH "Fallback directory for
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/")
+option(USE_LIBHANDY "Enable features that require libhandy (default)" ON)
+
find_package(nlohmann_json REQUIRED)
find_package(CURL)
find_package(ZLIB REQUIRED)
@@ -17,30 +19,30 @@ set(USE_TLS TRUE)
set(USE_OPEN_SSL TRUE)
find_package(IXWebSocket QUIET)
if (NOT IXWebSocket_FOUND)
- message("ixwebsocket was not found and will be included as a submodule")
- add_subdirectory(subprojects/ixwebsocket)
- include_directories(IXWEBSOCKET_INCLUDE_DIRS)
-endif()
+ message("ixwebsocket was not found and will be included as a submodule")
+ add_subdirectory(subprojects/ixwebsocket)
+ include_directories(IXWEBSOCKET_INCLUDE_DIRS)
+endif ()
-if(MINGW OR WIN32)
- link_libraries(ws2_32)
-endif()
+if (MINGW OR WIN32)
+ link_libraries(ws2_32)
+endif ()
-if(WIN32)
- add_compile_definitions(_CRT_SECURE_NO_WARNINGS)
- add_compile_definitions(NOMINMAX)
+if (WIN32)
+ add_compile_definitions(_CRT_SECURE_NO_WARNINGS)
+ add_compile_definitions(NOMINMAX)
- find_package(Fontconfig REQUIRED)
- link_libraries(${Fontconfig_LIBRARIES})
-endif()
+ find_package(Fontconfig REQUIRED)
+ link_libraries(${Fontconfig_LIBRARIES})
+endif ()
configure_file(${PROJECT_SOURCE_DIR}/src/config.h.in ${PROJECT_BINARY_DIR}/config.h)
file(GLOB_RECURSE ABADDON_SOURCES
- "src/*.h"
- "src/*.hpp"
- "src/*.cpp"
-)
+ "src/*.h"
+ "src/*.hpp"
+ "src/*.cpp"
+ )
add_executable(abaddon ${ABADDON_SOURCES})
target_include_directories(abaddon PUBLIC ${PROJECT_SOURCE_DIR}/src)
@@ -51,36 +53,48 @@ target_include_directories(abaddon PUBLIC ${SQLite3_INCLUDE_DIRS})
target_include_directories(abaddon PUBLIC ${NLOHMANN_JSON_INCLUDE_DIRS})
if ((CMAKE_CXX_COMPILER_ID STREQUAL "GNU") OR
- (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND
- ((CMAKE_SYSTEM_NAME STREQUAL "Linux") OR (CMAKE_CXX_COMPILER_VERSION LESS 9))))
- target_link_libraries(abaddon stdc++fs)
-endif()
+(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND
+((CMAKE_SYSTEM_NAME STREQUAL "Linux") OR (CMAKE_CXX_COMPILER_VERSION LESS 9))))
+ target_link_libraries(abaddon stdc++fs)
+endif ()
if (IXWebSocket_LIBRARIES)
- target_link_libraries(abaddon ${IXWebSocket_LIBRARIES})
- find_library(MBEDTLS_X509_LIBRARY mbedx509)
- find_library(MBEDTLS_TLS_LIBRARY mbedtls)
- find_library(MBEDTLS_CRYPTO_LIBRARY mbedcrypto)
- if (MBEDTLS_TLS_LIBRARY)
- target_link_libraries(abaddon ${MBEDTLS_TLS_LIBRARY})
- endif()
- if (MBEDTLS_X509_LIBRARY)
- target_link_libraries(abaddon ${MBEDTLS_X509_LIBRARY})
- endif()
- if (MBEDTLS_CRYPTO_LIBRARY)
- target_link_libraries(abaddon ${MBEDTLS_CRYPTO_LIBRARY})
- endif()
-else()
- target_link_libraries(abaddon $<BUILD_INTERFACE:ixwebsocket>)
-endif()
+ target_link_libraries(abaddon ${IXWebSocket_LIBRARIES})
+ find_library(MBEDTLS_X509_LIBRARY mbedx509)
+ find_library(MBEDTLS_TLS_LIBRARY mbedtls)
+ find_library(MBEDTLS_CRYPTO_LIBRARY mbedcrypto)
+ if (MBEDTLS_TLS_LIBRARY)
+ target_link_libraries(abaddon ${MBEDTLS_TLS_LIBRARY})
+ endif ()
+ if (MBEDTLS_X509_LIBRARY)
+ target_link_libraries(abaddon ${MBEDTLS_X509_LIBRARY})
+ endif ()
+ if (MBEDTLS_CRYPTO_LIBRARY)
+ target_link_libraries(abaddon ${MBEDTLS_CRYPTO_LIBRARY})
+ endif ()
+else ()
+ target_link_libraries(abaddon $<BUILD_INTERFACE:ixwebsocket>)
+endif ()
find_package(Threads)
if (Threads_FOUND)
- target_link_libraries(abaddon Threads::Threads)
-endif()
+ target_link_libraries(abaddon Threads::Threads)
+endif ()
target_link_libraries(abaddon ${SQLite3_LIBRARIES})
target_link_libraries(abaddon ${GTKMM_LIBRARIES})
target_link_libraries(abaddon ${CURL_LIBRARIES})
target_link_libraries(abaddon ${ZLIB_LIBRARY})
target_link_libraries(abaddon ${NLOHMANN_JSON_LIBRARIES})
+
+if (USE_LIBHANDY)
+ find_package(libhandy)
+ if (NOT libhandy_FOUND)
+ message("libhandy could not be found. features requiring it have been disabled")
+ set(USE_LIBHANDY OFF)
+ else ()
+ target_include_directories(abaddon PUBLIC ${libhandy_INCLUDE_DIRS})
+ target_link_libraries(abaddon ${libhandy_LIBRARIES})
+ target_compile_definitions(abaddon PRIVATE WITH_LIBHANDY)
+ endif ()
+endif ()
diff --git a/ci/msys-deps.txt b/ci/msys-deps.txt
index ab98a15..f628d09 100644
--- a/ci/msys-deps.txt
+++ b/ci/msys-deps.txt
@@ -31,6 +31,7 @@
/bin/libgraphite2.dll
/bin/libgtk-3-0.dll
/bin/libgtkmm-3.0-1.dll
+/bin/libhandy-1-0.dll
/bin/libharfbuzz-0.dll
/bin/libiconv-2.dll
/bin/libidn2-0.dll
diff --git a/cmake/Findgdk.cmake b/cmake/Findgdk.cmake
new file mode 100644
index 0000000..b3975c4
--- /dev/null
+++ b/cmake/Findgdk.cmake
@@ -0,0 +1,37 @@
+find_package(PkgConfig)
+if (PKG_CONFIG_FOUND)
+ pkg_check_modules(PC_gdk QUIET gdk-3.0)
+ set(gdk_DEFINITIONS ${PC_gdk_CFLAGS_OTHER})
+endif ()
+
+set(gdk_INCLUDE_HINTS ${PC_gdk_INCLUDEDIR} ${PC_gdk_INCLUDE_DIRS})
+set(gdk_LIBRARY_HINTS ${PC_gdk_LIBDIR} ${PC_gdk_LIBRARY_DIRS})
+
+find_path(gdk_INCLUDE_DIR
+ NAMES gdk/gdk.h
+ HINTS ${gdk_INCLUDE_HINTS}
+ /usr/include
+ /usr/local/include
+ /opt/local/include
+ PATH_SUFFIXES gdk-3.0)
+
+find_library(gdk_LIBRARY
+ NAMES gdk-3.0
+ gdk-3
+ gdk
+ HINTS ${gdk_LIBRARY_HINTS}
+ /usr/lib
+ /usr/local/lib
+ /opt/local/lib)
+
+set(gdk_LIBRARIES ${gdk_LIBRARY})
+set(gdk_INCLUDE_DIRS ${gdk_INCLUDE_DIR})
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(gdk
+ REQUIRED_VARS
+ gdk_LIBRARY
+ gdk_INCLUDE_DIR
+ VERSION_VAR gdk_VERSION)
+
+mark_as_advanced(gdk_INCLUDE_DIR gdk_LIBRARY)
diff --git a/cmake/Findgdkmm.cmake b/cmake/Findgdkmm.cmake
index b619270..5316bb7 100644
--- a/cmake/Findgdkmm.cmake
+++ b/cmake/Findgdkmm.cmake
@@ -1,48 +1,50 @@
-set(GDKMM_LIBRARY_NAME gdkmm-3.0)
+set(gdkmm_LIBRARY_NAME gdkmm-3.0)
find_package(PkgConfig)
if (PKG_CONFIG_FOUND)
- pkg_check_modules(PKGCONFIG_GDKMM QUIET ${GDKMM_LIBRARY_NAME})
- set(GDKMM_DEFINITIONS ${PKGCONFIG_GDKMM_CFLAGS_OTHER})
+ pkg_check_modules(PKGCONFIG_gdkmm QUIET ${gdkmm_LIBRARY_NAME})
+ set(gdkmm_DEFINITIONS ${PKGCONFIG_gdkmm_CFLAGS_OTHER})
endif (PKG_CONFIG_FOUND)
-set(GDKMM_INCLUDE_HINTS ${PKGCONFIG_GDKMM_INCLUDEDIR} ${PKGCONFIG_GDKMM_INCLUDE_DIRS})
-set(GDKMM_LIBRARY_HINTS ${PKGCONFIG_GDKMM_LIBDIR} ${PKGCONFIG_GDKMM_LIBRARY_DIRS})
+set(gdkmm_INCLUDE_HINTS ${PKGCONFIG_gdkmm_INCLUDEDIR} ${PKGCONFIG_gdkmm_INCLUDE_DIRS})
+set(gdkmm_LIBRARY_HINTS ${PKGCONFIG_gdkmm_LIBDIR} ${PKGCONFIG_gdkmm_LIBRARY_DIRS})
-find_path(GDKMM_INCLUDE_DIR
+find_path(gdkmm_INCLUDE_DIR
NAMES gdkmm.h
- HINTS ${GDKMM_INCLUDE_HINTS}
+ HINTS ${gdkmm_INCLUDE_HINTS}
/usr/include
/usr/local/include
/opt/local/include
- PATH_SUFFIXES ${GDKMM_LIBRARY_NAME})
+ PATH_SUFFIXES ${gdkmm_LIBRARY_NAME})
-find_path(GDKMM_CONFIG_INCLUDE_DIR
+find_path(gdkmm_CONFIG_INCLUDE_DIR
NAMES gdkmmconfig.h
- HINTS ${GDKMM_LIBRARY_HINTS}
+ HINTS ${gdkmm_LIBRARY_HINTS}
/usr/lib
/usr/local/lib
/opt/local/lib
- PATH_SUFFIXES ${GDKMM_LIBRARY_NAME}/include)
+ PATH_SUFFIXES ${gdkmm_LIBRARY_NAME}/include)
-find_library(GDKMM_LIBRARY
- NAMES ${GDKMM_LIBRARY_NAME}
- gdkmm
- HINTS ${GDKMM_LIBRARY_HINTS}
+find_library(gdkmm_LIBRARY
+ NAMES ${gdkmm_LIBRARY_NAME}
+ gdkmm
+ HINTS ${gdkmm_LIBRARY_HINTS}
/usr/lib
/usr/local/lib
/opt/local/lib
- PATH_SUFFIXES ${GDKMM_LIBRARY_NAME}
- ${GDKMM_LIBRARY_NAME}/include)
+ PATH_SUFFIXES ${gdkmm_LIBRARY_NAME}
+ ${gdkmm_LIBRARY_NAME}/include)
-set(GDKMM_LIBRARIES ${GDKMM_LIBRARY})
-set(GDKMM_INCLUDE_DIRS ${GDKMM_INCLUDE_DIR};${GDKMM_CONFIG_INCLUDE_DIRS};${GDKMM_CONFIG_INCLUDE_DIR})
+find_package(gdk)
+
+set(gdkmm_LIBRARIES ${gdkmm_LIBRARY};${gdk_LIBRARIES})
+set(gdkmm_INCLUDE_DIRS ${gdkmm_INCLUDE_DIR};${gdkmm_CONFIG_INCLUDE_DIRS};${gdkmm_CONFIG_INCLUDE_DIR};${gdk_INCLUDE_DIRS})
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(gdkmm
REQUIRED_VARS
- GDKMM_LIBRARY
- GDKMM_INCLUDE_DIRS
- VERSION_VAR GDKMM_VERSION)
+ gdkmm_LIBRARY
+ gdkmm_INCLUDE_DIRS
+ VERSION_VAR gdkmm_VERSION)
-mark_as_advanced(GDKMM_INCLUDE_DIR GDKMM_LIBRARY)
+mark_as_advanced(gdkmm_INCLUDE_DIR gdkmm_LIBRARY)
diff --git a/cmake/Findglib.cmake b/cmake/Findglib.cmake
index b2c730b..6a4af66 100644
--- a/cmake/Findglib.cmake
+++ b/cmake/Findglib.cmake
@@ -2,56 +2,70 @@ find_package(PkgConfig)
pkg_check_modules(PC_GLIB2 QUIET glib-2.0)
find_path(GLIB_INCLUDE_DIR
- NAMES glib.h
- HINTS ${PC_GLIB2_INCLUDEDIR}
- ${PC_GLIB2_INCLUDE_DIRS}
- $ENV{GLIB2_HOME}/include
- $ENV{GLIB2_ROOT}/include
- /usr/local/include
- /usr/include
- /glib2/include
- /glib-2.0/include
- PATH_SUFFIXES glib2 glib-2.0 glib-2.0/include
-)
+ NAMES glib.h
+ HINTS ${PC_GLIB2_INCLUDEDIR}
+ ${PC_GLIB2_INCLUDE_DIRS}
+ $ENV{GLIB2_HOME}/include
+ $ENV{GLIB2_ROOT}/include
+ /usr/local/include
+ /usr/include
+ /glib2/include
+ /glib-2.0/include
+ PATH_SUFFIXES glib2 glib-2.0 glib-2.0/include
+ )
set(GLIB_INCLUDE_DIRS ${GLIB_INCLUDE_DIR})
find_library(GLIB_LIBRARIES
- NAMES glib2
- glib-2.0
- HINTS ${PC_GLIB2_LIBDIR}
- ${PC_GLIB2_LIBRARY_DIRS}
- $ENV{GLIB2_HOME}/lib
- $ENV{GLIB2_ROOT}/lib
- /usr/local/lib
- /usr/lib
- /lib
- /glib-2.0/lib
- PATH_SUFFIXES glib2 glib-2.0
-)
+ NAMES glib2
+ glib-2.0
+ HINTS ${PC_GLIB2_LIBDIR}
+ ${PC_GLIB2_LIBRARY_DIRS}
+ $ENV{GLIB2_HOME}/lib
+ $ENV{GLIB2_ROOT}/lib
+ /usr/local/lib
+ /usr/lib
+ /lib
+ /glib-2.0/lib
+ PATH_SUFFIXES glib2 glib-2.0
+ )
+
+find_library(glib_GOBJECT_LIBRARIES
+ NAMES gobject-2.0
+ HINTS ${PC_GLIB2_LIBDIR}
+ ${PC_GLIB2_LIBRARY_DIRS}
+ )
+
+find_library(glib_GIO_LIBRARIES
+ NAMES gio-2.0
+ HINTS ${PC_GLIB2_LIBDIR}
+ ${PC_GLIB2_LIBRARY_DIRS}
+ )
get_filename_component(_GLIB2_LIB_DIR "${GLIB_LIBRARIES}" PATH)
find_path(GLIB_CONFIG_INCLUDE_DIR
- NAMES glibconfig.h
- HINTS ${PC_GLIB2_INCLUDEDIR}
- ${PC_GLIB2_INCLUDE_DIRS}
- $ENV{GLIB2_HOME}/include
- $ENV{GLIB2_ROOT}/include
- /usr/local/include
- /usr/include
- /glib2/include
- /glib-2.0/include
- ${_GLIB2_LIB_DIR}
- ${CMAKE_SYSTEM_LIBRARY_PATH}
- PATH_SUFFIXES glib2 glib-2.0 glib-2.0/include
-)
+ NAMES glibconfig.h
+ HINTS ${PC_GLIB2_INCLUDEDIR}
+ ${PC_GLIB2_INCLUDE_DIRS}
+ $ENV{GLIB2_HOME}/include
+ $ENV{GLIB2_ROOT}/include
+ /usr/local/include
+ /usr/include
+ /glib2/include
+ /glib-2.0/include
+ ${_GLIB2_LIB_DIR}
+ ${CMAKE_SYSTEM_LIBRARY_PATH}
+ PATH_SUFFIXES glib2 glib-2.0 glib-2.0/include
+ )
if (GLIB_CONFIG_INCLUDE_DIR)
set(GLIB_INCLUDE_DIRS ${GLIB_INCLUDE_DIRS} ${GLIB_CONFIG_INCLUDE_DIR})
-endif()
+endif ()
+
+set(GLIB_LIBRARIES ${GLIB_LIBRARIES} ${glib_GOBJECT_LIBRARIES} ${glib_GIO_LIBRARIES})
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(glib
- REQUIRED_VARS
- GLIB_LIBRARIES
- GLIB_INCLUDE_DIRS
- VERSION_VAR GLIB_VERSION)
-mark_as_advanced(GLIB_INCLUDE_DIR GLIB_CONFIG_INCLUDE_DIR)
+ REQUIRED_VARS
+ GLIB_LIBRARIES
+ GLIB_INCLUDE_DIRS
+ VERSION_VAR GLIB_VERSION)
+mark_as_advanced(GLIB_INCLUDE_DIR GLIB_CONFIG_INCLUDE_DIR glib_GOBJECT_LIBRARIES)
diff --git a/cmake/Findgtkmm.cmake b/cmake/Findgtkmm.cmake
index 479c1f3..addbede 100644
--- a/cmake/Findgtkmm.cmake
+++ b/cmake/Findgtkmm.cmake
@@ -1,13 +1,13 @@
-set(GTKMM_LIBRARY_NAME gtkmm-3.0)
-set(GDKMM_LIBRARY_NAME gdkmm-3.0)
+set(GTKMM_LIBRARY_NAME gtkmm-3.0)
+set(GDKMM_LIBRARY_NAME gdkmm-3.0)
find_package(PkgConfig)
-if(PKG_CONFIG_FOUND)
+if (PKG_CONFIG_FOUND)
pkg_check_modules(PC_GTKMM QUIET ${GTKMM_LIBRARY_NAME})
pkg_check_modules(PC_GDKMM QUIET ${GDKMM_LIBRARY_NAME})
pkg_check_modules(PC_PANGOMM QUIET ${PANGOMM_LIBRARY_NAME})
- set(GTKMM_DEFINITIONS ${PC_GTKMM_CFLAGS_OTHER})
-endif()
+ set(GTKMM_DEFINITIONS ${PC_GTKMM_CFLAGS_OTHER})
+endif ()
find_package(gtk)
find_package(glibmm)
@@ -46,14 +46,14 @@ find_path(GDKMM_CONFIG_INCLUDE_DIR
HINTS ${GDKMM_INCLUDE_HINTS}
PATH_SUFFIXES ${GDKMM_LIBRARY_NAME}/include)
-set(GTKMM_LIBRARIES ${GTKMM_LIB};${GDKMM_LIBRARY};${GTK_LIBRARIES};${GLIBMM_LIBRARIES};${PANGOMM_LIBRARIES};${CAIROMM_LIBRARIES};${ATKMM_LIBRARIES};${SIGC++_LIBRARIES})
-set(GTKMM_INCLUDE_DIRS ${GTKMM_INCLUDE_DIR};${GTKMM_CONFIG_INCLUDE_DIR};${GDKMM_INCLUDE_DIR};${GDKMM_CONFIG_INCLUDE_DIR};${GTK_INCLUDE_DIRS};${GLIBMM_INCLUDE_DIRS};${PANGOMM_INCLUDE_DIRS};${CAIROMM_INCLUDE_DIRS};${ATKMM_INCLUDE_DIRS};${SIGC++_INCLUDE_DIRS})
+set(GTKMM_LIBRARIES ${GTKMM_LIB};${gdkmm_LIBRARIES};${GTK_LIBRARIES};${GLIBMM_LIBRARIES};${PANGOMM_LIBRARIES};${CAIROMM_LIBRARIES};${ATKMM_LIBRARIES};${SIGC++_LIBRARIES})
+set(GTKMM_INCLUDE_DIRS ${GTKMM_INCLUDE_DIR};${GTKMM_CONFIG_INCLUDE_DIR};${gdkmm_INCLUDE_DIRS};${gdkmm_CONFIG_INCLUDE_DIR};${GTK_INCLUDE_DIRS};${GLIBMM_INCLUDE_DIRS};${PANGOMM_INCLUDE_DIRS};${CAIROMM_INCLUDE_DIRS};${ATKMM_INCLUDE_DIRS};${SIGC++_INCLUDE_DIRS})
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(gtkmm
REQUIRED_VARS
- GTKMM_LIB
- GTKMM_INCLUDE_DIRS
+ GTKMM_LIB
+ GTKMM_INCLUDE_DIRS
VERSION_VAR GTKMM_VERSION)
mark_as_advanced(GTKMM_INCLUDE_DIR GTKMM_LIBRARY)
diff --git a/cmake/Findlibhandy.cmake b/cmake/Findlibhandy.cmake
new file mode 100644
index 0000000..73792f0
--- /dev/null
+++ b/cmake/Findlibhandy.cmake
@@ -0,0 +1,39 @@
+set(libhandy_LIBRARY_NAME libhandy-1)
+
+find_package(PkgConfig)
+if (PKG_CONFIG_FOUND)
+ pkg_check_modules(PC_libhandy QUIET ${libhandy_LIBRARY_NAME})
+ set(libhandy_DEfINITIONS ${PC_libhandy_CFLAGS_OTHER})
+endif (PKG_CONFIG_FOUND)
+
+set(libhandy_INCLUDE_HINTS ${PC_libhandy_INCLUDEDIR} ${PC_libhandy_INCLUDE_DIRS})
+set(libhandy_LIBRARY_HINTS ${PC_libhandy_LIBDIR} ${PC_libhandy_LIBRARY_DIRS})
+
+find_path(libhandy_INCLUDE_DIR
+ NAMES handy.h
+ HINTS ${libhandy_INCLUDE_HINTS}
+ /usr/include
+ /usr/local/include
+ /opt/local/include
+ PATH_SUFFIXES ${libhandy_LIBRARY_NAME})
+
+find_library(libhandy_LIBRARY
+ NAMES ${libhandy_LIBRARY_NAME} handy-1
+ HINTS ${libhandy_LIBRARY_HINTS}
+ /usr/lib
+ /usr/local/lib
+ /opt/local/lib
+ PATH_SUFFIXES ${libhandy_LIBRARY_NAME}
+ ${libhandy_LIBRARY_NAME}/include)
+
+set(libhandy_LIBRARIES ${libhandy_LIBRARY})
+set(libhandy_INCLUDE_DIRS ${libhandy_INCLUDE_DIR};${libhandy_CONFIG_INCLUDE_DIRS})
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(libhandy
+ REQUIRED_VARS
+ libhandy_LIBRARY
+ libhandy_INCLUDE_DIR
+ VERSION_VAR libhandy_VERSION)
+
+mark_as_advanced(libhandy_INCLUDE_DIR libhandy_LIBRARY)
diff --git a/res/css/application-low-priority.css b/res/css/application-low-priority.css
index cf108f4..d92b964 100644
--- a/res/css/application-low-priority.css
+++ b/res/css/application-low-priority.css
@@ -3,6 +3,31 @@ application wide stuff
has to be separate to allow main.css to override certain things
*/
+.app-window tabbar .box {
+ margin: -7px -1px -7px -1px;
+ background: #2a2a2a;
+ border: 1px solid black;
+}
+
+.app-window tabbar tab:hover {
+ box-shadow: inset 0 -7px lighter(blue);
+}
+
+.app-window tabbar tab:checked {
+ box-shadow: inset 0 -7px blue;
+}
+
+.app-window tabbar tab {
+ background: #1A1A1A;
+ border: 1px solid #808080;
+}
+
+.app-window tabbar tab.needs-attention:not(:checked) {
+ font-weight: bold;
+ animation: 150ms ease-in;
+ background-image: radial-gradient(ellipse at bottom, #FF5370, #1A1A1A 30%);
+}
+
.app-window label:not(:disabled) {
color: @text_color;
}
diff --git a/res/res.7z b/res/res.7z
new file mode 100644
index 0000000..ef87987
--- /dev/null
+++ b/res/res.7z
Binary files differ
diff --git a/src/abaddon.cpp b/src/abaddon.cpp
index a2d65e5..4ca1462 100644
--- a/src/abaddon.cpp
+++ b/src/abaddon.cpp
@@ -17,6 +17,10 @@
#include "windows/pinnedwindow.hpp"
#include "windows/threadswindow.hpp"
+#ifdef WITH_LIBHANDY
+ #include <handy.h>
+#endif
+
#ifdef _WIN32
#pragma comment(lib, "crypt32.lib")
#endif
@@ -59,9 +63,48 @@ Abaddon &Abaddon::Get() {
return instance;
}
+#ifdef WITH_LIBHANDY
+ #ifdef _WIN32
+constexpr static guint BUTTON_BACK = 4;
+constexpr static guint BUTTON_FORWARD = 5;
+ #else
+constexpr static guint BUTTON_BACK = 8;
+constexpr static guint BUTTON_FORWARD = 9;
+ #endif
+
+static void HandleButtonEvents(GdkEvent *event, MainWindow *main_window) {
+ if (event->type != GDK_BUTTON_PRESS) return;
+
+ auto *widget = gtk_get_event_widget(event);
+ if (widget == nullptr) return;
+ auto *window = gtk_widget_get_toplevel(widget);
+ if (static_cast<void *>(window) != static_cast<void *>(main_window->gobj())) return; // is this the right way???
+
+ switch (event->button.button) {
+ case BUTTON_BACK:
+ main_window->GoBack();
+ break;
+ case BUTTON_FORWARD:
+ main_window->GoForward();
+ break;
+ }
+}
+
+static void MainEventHandler(GdkEvent *event, void *main_window) {
+ HandleButtonEvents(event, static_cast<MainWindow *>(main_window));
+ gtk_main_do_event(event);
+}
+#endif
+
int Abaddon::StartGTK() {
m_gtk_app = Gtk::Application::create("com.github.uowuo.abaddon");
+#ifdef WITH_LIBHANDY
+ m_gtk_app->signal_activate().connect([] {
+ hdy_init();
+ });
+#endif
+
m_css_provider = Gtk::CssProvider::create();
m_css_provider->signal_parsing_error().connect([](const Glib::RefPtr<const Gtk::CssSection> &section, const Glib::Error &error) {
Gtk::MessageDialog dlg("css failed parsing (" + error.what() + ")", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
@@ -103,6 +146,10 @@ int Abaddon::StartGTK() {
m_main_window->set_title(APP_TITLE);
m_main_window->set_position(Gtk::WIN_POS_CENTER);
+#ifdef WITH_LIBHANDY
+ gdk_event_handler_set(&MainEventHandler, m_main_window.get(), nullptr);
+#endif
+
if (!m_settings.IsValid()) {
Gtk::MessageDialog dlg(*m_main_window, "The settings file could not be opened!", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
dlg.set_position(Gtk::WIN_POS_CENTER);
@@ -138,7 +185,7 @@ int Abaddon::StartGTK() {
m_main_window->signal_action_view_pins().connect(sigc::mem_fun(*this, &Abaddon::ActionViewPins));
m_main_window->signal_action_view_threads().connect(sigc::mem_fun(*this, &Abaddon::ActionViewThreads));
- m_main_window->GetChannelList()->signal_action_channel_item_select().connect(sigc::mem_fun(*this, &Abaddon::ActionChannelOpened));
+ m_main_window->GetChannelList()->signal_action_channel_item_select().connect(sigc::bind(sigc::mem_fun(*this, &Abaddon::ActionChannelOpened), true));
m_main_window->GetChannelList()->signal_action_guild_leave().connect(sigc::mem_fun(*this, &Abaddon::ActionLeaveGuild));
m_main_window->GetChannelList()->signal_action_guild_settings().connect(sigc::mem_fun(*this, &Abaddon::ActionGuildSettings));
@@ -414,6 +461,9 @@ void Abaddon::SaveState() {
AbaddonApplicationState state;
state.ActiveChannel = m_main_window->GetChatActiveChannel();
state.Expansion = m_main_window->GetChannelList()->GetExpansionState();
+#ifdef WITH_LIBHANDY
+ state.Tabs = m_main_window->GetChatWindow()->GetTabsState();
+#endif
const auto path = GetStateCachePath();
if (!util::IsFolder(path)) {
@@ -440,6 +490,9 @@ void Abaddon::LoadState() {
try {
AbaddonApplicationState state = nlohmann::json::parse(data.begin(), data.end());
m_main_window->GetChannelList()->UseExpansionState(state.Expansion);
+#ifdef WITH_LIBHANDY
+ m_main_window->GetChatWindow()->UseTabsState(state.Tabs);
+#endif
ActionChannelOpened(state.ActiveChannel);
} catch (const std::exception &e) {
printf("failed to load application state: %s\n", e.what());
@@ -551,7 +604,7 @@ void Abaddon::ActionJoinGuildDialog() {
}
}
-void Abaddon::ActionChannelOpened(Snowflake id) {
+void Abaddon::ActionChannelOpened(Snowflake id, bool expand_to) {
if (!id.IsValid() || id == m_main_window->GetChatActiveChannel()) return;
m_main_window->GetChatWindow()->SetTopic("");
@@ -574,7 +627,7 @@ void Abaddon::ActionChannelOpened(Snowflake id) {
display = "Empty group";
m_main_window->set_title(std::string(APP_TITLE) + " - " + display);
}
- m_main_window->UpdateChatActiveChannel(id);
+ m_main_window->UpdateChatActiveChannel(id, expand_to);
if (m_channels_requested.find(id) == m_channels_requested.end()) {
// dont fire requests we know will fail
if (can_access) {
diff --git a/src/abaddon.hpp b/src/abaddon.hpp
index 3404633..3296c45 100644
--- a/src/abaddon.hpp
+++ b/src/abaddon.hpp
@@ -35,7 +35,7 @@ public:
void ActionDisconnect();
void ActionSetToken();
void ActionJoinGuildDialog();
- void ActionChannelOpened(Snowflake id);
+ void ActionChannelOpened(Snowflake id, bool expand_to = true);
void ActionChatInputSubmit(std::string msg, Snowflake channel, Snowflake referenced_message);
void ActionChatLoadHistory(Snowflake id);
void ActionChatEditMessage(Snowflake channel_id, Snowflake id);
diff --git a/src/components/channels.cpp b/src/components/channels.cpp
index 4a6b1bc..2b83eb0 100644
--- a/src/components/channels.cpp
+++ b/src/components/channels.cpp
@@ -17,6 +17,10 @@ ChannelList::ChannelList()
, m_menu_category_copy_id("_Copy ID", true)
, m_menu_channel_copy_id("_Copy ID", true)
, m_menu_channel_mark_as_read("Mark as _Read", true)
+#ifdef WITH_LIBHANDY
+ , m_menu_channel_open_tab("Open in New _Tab", true)
+ , m_menu_dm_open_tab("Open in New _Tab", true)
+#endif
, m_menu_dm_copy_id("_Copy ID", true)
, m_menu_dm_close("") // changes depending on if group or not
, m_menu_thread_copy_id("_Copy ID", true)
@@ -143,6 +147,15 @@ ChannelList::ChannelList()
else
discord.MuteChannel(id, NOOP_CALLBACK);
});
+
+#ifdef WITH_LIBHANDY
+ m_menu_channel_open_tab.signal_activate().connect([this] {
+ const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
+ m_signal_action_open_new_tab.emit(id);
+ });
+ m_menu_channel.append(m_menu_channel_open_tab);
+#endif
+
m_menu_channel.append(m_menu_channel_mark_as_read);
m_menu_channel.append(m_menu_channel_toggle_mute);
m_menu_channel.append(m_menu_channel_copy_id);
@@ -170,6 +183,13 @@ ChannelList::ChannelList()
else
discord.MuteChannel(id, NOOP_CALLBACK);
});
+#ifdef WITH_LIBHANDY
+ m_menu_dm_open_tab.signal_activate().connect([this] {
+ const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
+ m_signal_action_open_new_tab.emit(id);
+ });
+ m_menu_dm.append(m_menu_dm_open_tab);
+#endif
m_menu_dm.append(m_menu_dm_toggle_mute);
m_menu_dm.append(m_menu_dm_close);
m_menu_dm.append(m_menu_dm_copy_id);
@@ -442,7 +462,7 @@ void ChannelList::OnGuildUnmute(Snowflake id) {
// create a temporary channel row for non-joined threads
// and delete them when the active channel switches off of them if still not joined
-void ChannelList::SetActiveChannel(Snowflake id) {
+void ChannelList::SetActiveChannel(Snowflake id, bool expand_to) {
// mark channel as read when switching off
if (m_active_channel.IsValid())
Abaddon::Get().GetDiscordClient().MarkChannelAsRead(m_active_channel, [](...) {});
@@ -459,11 +479,12 @@ void ChannelList::SetActiveChannel(Snowflake id) {
const auto channel_iter = GetIteratorForChannelFromID(id);
if (channel_iter) {
- m_view.expand_to_path(m_model->get_path(channel_iter));
+ if (expand_to) {
+ m_view.expand_to_path(m_model->get_path(channel_iter));
+ }
m_view.get_selection()->select(channel_iter);
} else {
m_view.get_selection()->unselect_all();
- // SetActiveChannel should probably just take the channel object
const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id);
if (!channel.has_value() || !channel->IsThread()) return;
auto parent_iter = GetIteratorForChannelFromID(*channel->ParentID);
@@ -960,6 +981,12 @@ ChannelList::type_signal_action_guild_settings ChannelList::signal_action_guild_
return m_signal_action_guild_settings;
}
+#ifdef WITH_LIBHANDY
+ChannelList::type_signal_action_open_new_tab ChannelList::signal_action_open_new_tab() {
+ return m_signal_action_open_new_tab;
+}
+#endif
+
ChannelList::ModelColumns::ModelColumns() {
add(m_type);
add(m_id);
diff --git a/src/components/channels.hpp b/src/components/channels.hpp
index 044d0b5..53a68c9 100644
--- a/src/components/channels.hpp
+++ b/src/components/channels.hpp
@@ -19,7 +19,7 @@ public:
ChannelList();
void UpdateListing();
- void SetActiveChannel(Snowflake id);
+ void SetActiveChannel(Snowflake id, bool expand_to);
// channel list should be populated when this is called
void UseExpansionState(const ExpansionStateRoot &state);
@@ -121,11 +121,19 @@ protected:
Gtk::MenuItem m_menu_channel_mark_as_read;
Gtk::MenuItem m_menu_channel_toggle_mute;
+#ifdef WITH_LIBHANDY
+ Gtk::MenuItem m_menu_channel_open_tab;
+#endif
+
Gtk::Menu m_menu_dm;
Gtk::MenuItem m_menu_dm_copy_id;
Gtk::MenuItem m_menu_dm_close;
Gtk::MenuItem m_menu_dm_toggle_mute;
+#ifdef WITH_LIBHANDY
+ Gtk::MenuItem m_menu_dm_open_tab;
+#endif
+
Gtk::Menu m_menu_thread;
Gtk::MenuItem m_menu_thread_copy_id;
Gtk::MenuItem m_menu_thread_leave;
@@ -149,16 +157,25 @@ protected:
std::unordered_map<Snowflake, Gtk::TreeModel::iterator> m_tmp_channel_map;
public:
- typedef sigc::signal<void, Snowflake> type_signal_action_channel_item_select;
- typedef sigc::signal<void, Snowflake> type_signal_action_guild_leave;
- typedef sigc::signal<void, Snowflake> type_signal_action_guild_settings;
+ using type_signal_action_channel_item_select = sigc::signal<void, Snowflake>;
+ using type_signal_action_guild_leave = sigc::signal<void, Snowflake>;
+ using type_signal_action_guild_settings = sigc::signal<void, Snowflake>;
+
+#ifdef WITH_LIBHANDY
+ using type_signal_action_open_new_tab = sigc::signal<void, Snowflake>;
+ type_signal_action_open_new_tab signal_action_open_new_tab();
+#endif
type_signal_action_channel_item_select signal_action_channel_item_select();
type_signal_action_guild_leave signal_action_guild_leave();
type_signal_action_guild_settings signal_action_guild_settings();
-protected:
+private:
type_signal_action_channel_item_select m_signal_action_channel_item_select;
type_signal_action_guild_leave m_signal_action_guild_leave;
type_signal_action_guild_settings m_signal_action_guild_settings;
+
+#ifdef WITH_LIBHANDY
+ type_signal_action_open_new_tab m_signal_action_open_new_tab;
+#endif
};
diff --git a/src/components/channeltabswitcherhandy.cpp b/src/components/channeltabswitcherhandy.cpp
new file mode 100644
index 0000000..f7b0226
--- /dev/null
+++ b/src/components/channeltabswitcherhandy.cpp
@@ -0,0 +1,202 @@
+#ifdef WITH_LIBHANDY
+
+ #include "channeltabswitcherhandy.hpp"
+ #include "abaddon.hpp"
+
+void selected_page_notify_cb(HdyTabView *view, GParamSpec *pspec, ChannelTabSwitcherHandy *switcher) {
+ auto *page = hdy_tab_view_get_selected_page(view);
+ if (auto it = switcher->m_pages_rev.find(page); it != switcher->m_pages_rev.end()) {
+ switcher->m_signal_channel_switched_to.emit(it->second);
+ }
+}
+
+gboolean close_page_cb(HdyTabView *view, HdyTabPage *page, ChannelTabSwitcherHandy *switcher) {
+ switcher->ClearPage(page);
+ hdy_tab_view_close_page_finish(view, page, true);
+ return GDK_EVENT_STOP;
+}
+
+ChannelTabSwitcherHandy::ChannelTabSwitcherHandy() {
+ m_tab_bar = hdy_tab_bar_new();
+ m_tab_bar_wrapped = Glib::wrap(GTK_WIDGET(m_tab_bar));
+ m_tab_view = hdy_tab_view_new();
+ m_tab_view_wrapped = Glib::wrap(GTK_WIDGET(m_tab_view));
+
+ g_signal_connect(m_tab_view, "notify::selected-page", G_CALLBACK(selected_page_notify_cb), this);
+ g_signal_connect(m_tab_view, "close-page", G_CALLBACK(close_page_cb), this);
+
+ hdy_tab_bar_set_view(m_tab_bar, m_tab_view);
+ add(*m_tab_bar_wrapped);
+ m_tab_bar_wrapped->show();
+
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ discord.signal_message_create().connect([this](const Message &data) {
+ CheckUnread(data.ChannelID);
+ });
+
+ discord.signal_message_ack().connect([this](const MessageAckData &data) {
+ CheckUnread(data.ChannelID);
+ });
+}
+
+void ChannelTabSwitcherHandy::AddChannelTab(Snowflake id) {
+ if (m_pages.find(id) != m_pages.end()) return;
+
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ const auto channel = discord.GetChannel(id);
+ if (!channel.has_value()) return;
+
+ auto *dummy = Gtk::make_managed<Gtk::Box>(); // minimal
+ auto *page = hdy_tab_view_append(m_tab_view, GTK_WIDGET(dummy->gobj()));
+
+ hdy_tab_page_set_title(page, channel->GetDisplayName().c_str());
+ hdy_tab_page_set_tooltip(page, nullptr);
+
+ m_pages[id] = page;
+ m_pages_rev[page] = id;
+
+ CheckUnread(id);
+ CheckPageIcon(page, *channel);
+ AppendPageHistory(page, id);
+}
+
+void ChannelTabSwitcherHandy::ReplaceActiveTab(Snowflake id) {
+ auto *page = hdy_tab_view_get_selected_page(m_tab_view);
+ if (page == nullptr) {
+ AddChannelTab(id);
+ } else if (auto it = m_pages.find(id); it != m_pages.end()) {
+ hdy_tab_view_set_selected_page(m_tab_view, it->second);
+ } else {
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ const auto channel = discord.GetChannel(id);
+ if (!channel.has_value()) return;
+
+ hdy_tab_page_set_title(page, channel->GetDisplayName().c_str());
+
+ ClearPage(page);
+ m_pages[id] = page;
+ m_pages_rev[page] = id;
+
+ CheckUnread(id);
+ CheckPageIcon(page, *channel);
+ AppendPageHistory(page, id);
+ }
+}
+
+TabsState ChannelTabSwitcherHandy::GetTabsState() {
+ TabsState state;
+
+ const gint num_pages = hdy_tab_view_get_n_pages(m_tab_view);
+ for (gint i = 0; i < num_pages; i++) {
+ auto *page = hdy_tab_view_get_nth_page(m_tab_view, i);
+ if (page != nullptr) {
+ if (const auto it = m_pages_rev.find(page); it != m_pages_rev.end()) {
+ state.Channels.push_back(it->second);
+ }
+ }
+ }
+
+ return state;
+}
+
+void ChannelTabSwitcherHandy::UseTabsState(const TabsState &state) {
+ for (auto id : state.Channels) {
+ AddChannelTab(id);
+ }
+}
+
+void ChannelTabSwitcherHandy::GoBackOnCurrent() {
+ AdvanceOnCurrent(-1);
+}
+
+void ChannelTabSwitcherHandy::GoForwardOnCurrent() {
+ AdvanceOnCurrent(1);
+}
+
+int ChannelTabSwitcherHandy::GetNumberOfTabs() const {
+ return hdy_tab_view_get_n_pages(m_tab_view);
+}
+
+void ChannelTabSwitcherHandy::CheckUnread(Snowflake id) {
+ if (auto it = m_pages.find(id); it != m_pages.end()) {
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ const bool has_unreads = discord.GetUnreadStateForChannel(id) > -1;
+ const bool show_indicator = has_unreads && !discord.IsChannelMuted(id);
+ hdy_tab_page_set_needs_attention(it->second, show_indicator);
+ }
+}
+
+void ChannelTabSwitcherHandy::ClearPage(HdyTabPage *page) {
+ if (auto it = m_pages_rev.find(page); it != m_pages_rev.end()) {
+ m_pages.erase(it->second);
+ }
+ m_pages_rev.erase(page);
+ m_page_icons.erase(page);
+}
+
+void ChannelTabSwitcherHandy::OnPageIconLoad(HdyTabPage *page, const Glib::RefPtr<Gdk::Pixbuf> &pb) {
+ auto new_pb = pb->scale_simple(16, 16, Gdk::INTERP_BILINEAR);
+ m_page_icons[page] = new_pb;
+ hdy_tab_page_set_icon(page, G_ICON(new_pb->gobj()));
+}
+
+void ChannelTabSwitcherHandy::CheckPageIcon(HdyTabPage *page, const ChannelData &data) {
+ if (data.GuildID.has_value()) {
+ if (const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(*data.GuildID); guild.has_value() && guild->HasIcon()) {
+ auto *child_widget = hdy_tab_page_get_child(page);
+ if (child_widget == nullptr) return; // probably wont happen :---)
+ // i think this works???
+ auto *trackable = Glib::wrap(GTK_WIDGET(child_widget));
+
+ Abaddon::Get().GetImageManager().LoadFromURL(
+ guild->GetIconURL("png", "16"),
+ sigc::track_obj([this, page](const Glib::RefPtr<Gdk::Pixbuf> &pb) { OnPageIconLoad(page, pb); },
+ *trackable));
+ return;
+ }
+ return;
+ }
+
+ hdy_tab_page_set_icon(page, nullptr);
+}
+
+void ChannelTabSwitcherHandy::AppendPageHistory(HdyTabPage *page, Snowflake channel) {
+ auto it = m_page_history.find(page);
+ if (it == m_page_history.end()) {
+ m_page_history[page] = PageHistory { { channel }, 0 };
+ return;
+ }
+
+ // drop everything beyond current position
+ it->second.Visited.resize(++it->second.CurrentVisitedIndex);
+ it->second.Visited.push_back(channel);
+}
+
+void ChannelTabSwitcherHandy::AdvanceOnCurrent(size_t by) {
+ auto *current = hdy_tab_view_get_selected_page(m_tab_view);
+ if (current == nullptr) return;
+ auto history = m_page_history.find(current);
+ if (history == m_page_history.end()) return;
+ if (by + history->second.CurrentVisitedIndex < 0 || by + history->second.CurrentVisitedIndex >= history->second.Visited.size()) return;
+
+ history->second.CurrentVisitedIndex += by;
+ const auto to_id = history->second.Visited.at(history->second.CurrentVisitedIndex);
+
+ // temporarily point current index to the end so that it doesnt fuck up the history
+ // remove it immediately after cuz the emit will call ReplaceActiveTab
+ const auto real = history->second.CurrentVisitedIndex;
+ history->second.CurrentVisitedIndex = history->second.Visited.size() - 1;
+ m_signal_channel_switched_to.emit(to_id);
+ // iterator might not be valid
+ history = m_page_history.find(current);
+ if (history != m_page_history.end()) {
+ history->second.Visited.pop_back();
+ }
+ history->second.CurrentVisitedIndex = real;
+}
+
+ChannelTabSwitcherHandy::type_signal_channel_switched_to ChannelTabSwitcherHandy::signal_channel_switched_to() {
+ return m_signal_channel_switched_to;
+}
+
+#endif
diff --git a/src/components/channeltabswitcherhandy.hpp b/src/components/channeltabswitcherhandy.hpp
new file mode 100644
index 0000000..561d463
--- /dev/null
+++ b/src/components/channeltabswitcherhandy.hpp
@@ -0,0 +1,66 @@
+#pragma once
+// perhaps this should be conditionally included within cmakelists?
+#ifdef WITH_LIBHANDY
+ #include <gtkmm/box.h>
+ #include <unordered_map>
+ #include <handy.h>
+ #include "discord/snowflake.hpp"
+ #include "state.hpp"
+
+class ChannelData;
+
+// thin wrapper over c api
+// HdyTabBar + invisible HdyTabView since it needs one
+class ChannelTabSwitcherHandy : public Gtk::Box {
+public:
+ ChannelTabSwitcherHandy();
+
+ // no-op if already added
+ void AddChannelTab(Snowflake id);
+ // switches to existing tab if it exists
+ void ReplaceActiveTab(Snowflake id);
+ TabsState GetTabsState();
+ void UseTabsState(const TabsState &state);
+
+ void GoBackOnCurrent();
+ void GoForwardOnCurrent();
+
+ [[nodiscard]] int GetNumberOfTabs() const;
+
+private:
+ void CheckUnread(Snowflake id);
+ void ClearPage(HdyTabPage *page);
+ void OnPageIconLoad(HdyTabPage *page, const Glib::RefPtr<Gdk::Pixbuf> &pb);
+ void CheckPageIcon(HdyTabPage *page, const ChannelData &data);
+ void AppendPageHistory(HdyTabPage *page, Snowflake channel);
+ void AdvanceOnCurrent(size_t by);
+
+ HdyTabBar *m_tab_bar;
+ Gtk::Widget *m_tab_bar_wrapped;
+ HdyTabView *m_tab_view;
+ Gtk::Widget *m_tab_view_wrapped;
+
+ std::unordered_map<Snowflake, HdyTabPage *> m_pages;
+ std::unordered_map<HdyTabPage *, Snowflake> m_pages_rev;
+ // need to hold a reference to the pixbuf data
+ std::unordered_map<HdyTabPage *, Glib::RefPtr<Gdk::Pixbuf>> m_page_icons;
+
+ struct PageHistory {
+ std::vector<Snowflake> Visited;
+ size_t CurrentVisitedIndex;
+ };
+
+ std::unordered_map<HdyTabPage *, PageHistory> m_page_history;
+
+ friend void selected_page_notify_cb(HdyTabView *, GParamSpec *, ChannelTabSwitcherHandy *);
+ friend gboolean close_page_cb(HdyTabView *, HdyTabPage *, ChannelTabSwitcherHandy *);
+
+public:
+ using type_signal_channel_switched_to = sigc::signal<void, Snowflake>;
+
+ type_signal_channel_switched_to signal_channel_switched_to();
+
+private:
+ type_signal_channel_switched_to m_signal_channel_switched_to;
+};
+#endif
diff --git a/src/components/chatwindow.cpp b/src/components/chatwindow.cpp
index 582343d..8667488 100644
--- a/src/components/chatwindow.cpp
+++ b/src/components/chatwindow.cpp
@@ -4,6 +4,9 @@
#include "ratelimitindicator.hpp"
#include "chatinput.hpp"
#include "chatlist.hpp"
+#ifdef WITH_LIBHANDY
+ #include "channeltabswitcherhandy.hpp"
+#endif
ChatWindow::ChatWindow() {
Abaddon::Get().GetDiscordClient().signal_message_send_fail().connect(sigc::mem_fun(*this, &ChatWindow::OnMessageSendFail));
@@ -15,6 +18,13 @@ ChatWindow::ChatWindow() {
m_rate_limit_indicator = Gtk::manage(new RateLimitIndicator);
m_meta = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
+#ifdef WITH_LIBHANDY
+ m_tab_switcher = Gtk::make_managed<ChannelTabSwitcherHandy>();
+ m_tab_switcher->signal_channel_switched_to().connect([this](Snowflake id) {
+ m_signal_action_channel_click.emit(id, false);
+ });
+#endif
+
m_rate_limit_indicator->set_margin_end(5);
m_rate_limit_indicator->set_hexpand(true);
m_rate_limit_indicator->set_halign(Gtk::ALIGN_END);
@@ -55,7 +65,7 @@ ChatWindow::ChatWindow() {
m_completer.show();
m_chat->signal_action_channel_click().connect([this](Snowflake id) {
- m_signal_action_channel_click.emit(id);
+ m_signal_action_channel_click.emit(id, true);
});
m_chat->signal_action_chat_load_history().connect([this](Snowflake id) {
m_signal_action_chat_load_history.emit(id);
@@ -88,6 +98,10 @@ ChatWindow::ChatWindow() {
m_meta->add(*m_input_indicator);
m_meta->add(*m_rate_limit_indicator);
// m_scroll->add(*m_list);
+#ifdef WITH_LIBHANDY
+ m_main->add(*m_tab_switcher);
+ m_tab_switcher->show();
+#endif
m_main->add(m_topic);
m_main->add(*m_chat);
m_main->add(m_completer);
@@ -115,6 +129,10 @@ void ChatWindow::SetActiveChannel(Snowflake id) {
m_rate_limit_indicator->SetActiveChannel(id);
if (m_is_replying)
StopReplying();
+
+#ifdef WITH_LIBHANDY
+ m_tab_switcher->ReplaceActiveTab(id);
+#endif
}
void ChatWindow::AddNewMessage(const Message &data) {
@@ -150,6 +168,32 @@ void ChatWindow::SetTopic(const std::string &text) {
m_topic.set_visible(text.length() > 0);
}
+#ifdef WITH_LIBHANDY
+void ChatWindow::OpenNewTab(Snowflake id) {
+ // open if its the first tab (in which case it really isnt a tab but whatever)
+ if (m_tab_switcher->GetNumberOfTabs() == 0) {
+ m_signal_action_channel_click.emit(id, false);
+ }
+ m_tab_switcher->AddChannelTab(id);
+}
+
+TabsState ChatWindow::GetTabsState() {
+ return m_tab_switcher->GetTabsState();
+}
+
+void ChatWindow::UseTabsState(const TabsState &state) {
+ m_tab_switcher->UseTabsState(state);
+}
+
+void ChatWindow::GoBack() {
+ m_tab_switcher->GoBackOnCurrent();
+}
+
+void ChatWindow::GoForward() {
+ m_tab_switcher->GoForwardOnCurrent();
+}
+#endif
+
Snowflake ChatWindow::GetActiveChannel() const {
return m_active_channel;
}
diff --git a/src/components/chatwindow.hpp b/src/components/chatwindow.hpp
index 0f40e88..1c0b7cc 100644
--- a/src/components/chatwindow.hpp
+++ b/src/components/chatwindow.hpp
@@ -4,6 +4,11 @@
#include <set>
#include "discord/discord.hpp"
#include "completer.hpp"
+#include "state.hpp"
+
+#ifdef WITH_LIBHANDY
+class ChannelTabSwitcherHandy;
+#endif
class ChatMessageHeader;
class ChatMessageItemContainer;
@@ -25,11 +30,19 @@ public:
void DeleteMessage(Snowflake id); // add [deleted] indicator
void UpdateMessage(Snowflake id); // add [edited] indicator
void AddNewHistory(const std::vector<Message> &msgs); // prepend messages
- void InsertChatInput(const std::string& text);
+ void InsertChatInput(const std::string &text);
Snowflake GetOldestListedMessage(); // oldest message that is currently in the ListBox
void UpdateReactions(Snowflake id);
void SetTopic(const std::string &text);
+#ifdef WITH_LIBHANDY
+ void OpenNewTab(Snowflake id);
+ TabsState GetTabsState();
+ void UseTabsState(const TabsState &state);
+ void GoBack();
+ void GoForward();
+#endif
+
protected:
bool m_is_replying = false;
Snowflake m_replying_to;
@@ -62,14 +75,18 @@ protected:
RateLimitIndicator *m_rate_limit_indicator;
Gtk::Box *m_meta;
+#ifdef WITH_LIBHANDY
+ ChannelTabSwitcherHandy *m_tab_switcher;
+#endif
+
public:
- typedef sigc::signal<void, Snowflake, Snowflake> type_signal_action_message_edit;
- typedef sigc::signal<void, std::string, Snowflake, Snowflake> type_signal_action_chat_submit;
- typedef sigc::signal<void, Snowflake> type_signal_action_chat_load_history;
- typedef sigc::signal<void, Snowflake> type_signal_action_channel_click;
- typedef sigc::signal<void, Snowflake> type_signal_action_insert_mention;
- typedef sigc::signal<void, Snowflake, Glib::ustring> type_signal_action_reaction_add;
- typedef sigc::signal<void, Snowflake, Glib::ustring> type_signal_action_reaction_remove;
+ using type_signal_action_message_edit = sigc::signal<void, Snowflake, Snowflake>;
+ using type_signal_action_chat_submit = sigc::signal<void, std::string, Snowflake, Snowflake>;
+ using type_signal_action_chat_load_history = sigc::signal<void, Snowflake>;
+ using type_signal_action_channel_click = sigc::signal<void, Snowflake, bool>;
+ using type_signal_action_insert_mention = sigc::signal<void, Snowflake>;
+ using type_signal_action_reaction_add = sigc::signal<void, Snowflake, Glib::ustring>;
+ using type_signal_action_reaction_remove = sigc::signal<void, Snowflake, Glib::ustring>;
type_signal_action_message_edit signal_action_message_edit();
type_signal_action_chat_submit signal_action_chat_submit();
diff --git a/src/discord/channel.cpp b/src/discord/channel.cpp
index 0770581..6277341 100644
--- a/src/discord/channel.cpp
+++ b/src/discord/channel.cpp
@@ -92,6 +92,19 @@ std::string ChannelData::GetIconURL() const {
return "https://cdn.discordapp.com/channel-icons/" + std::to_string(ID) + "/" + *Icon + ".png";
}
+std::string ChannelData::GetDisplayName() const {
+ if (Name.has_value()) {
+ return "#" + *Name;
+ } else {
+ const auto recipients = GetDMRecipients();
+ if (Type == ChannelType::DM && !recipients.empty())
+ return recipients[0].Username;
+ else if (Type == ChannelType::GROUP_DM)
+ return std::to_string(recipients.size()) + " members";
+ }
+ return "Unknown";
+}
+
std::vector<Snowflake> ChannelData::GetChildIDs() const {
return Abaddon::Get().GetDiscordClient().GetChildChannelIDs(ID);
}
diff --git a/src/discord/channel.hpp b/src/discord/channel.hpp
index 8feeb92..77cf029 100644
--- a/src/discord/channel.hpp
+++ b/src/discord/channel.hpp
@@ -102,6 +102,7 @@ struct ChannelData {
[[nodiscard]] bool IsText() const noexcept;
[[nodiscard]] bool HasIcon() const noexcept;
[[nodiscard]] std::string GetIconURL() const;
+ [[nodiscard]] std::string GetDisplayName() const;
[[nodiscard]] std::vector<Snowflake> GetChildIDs() const;
[[nodiscard]] std::optional<PermissionOverwrite> GetOverwrite(Snowflake id) const;
[[nodiscard]] std::vector<UserData> GetDMRecipients() const;
diff --git a/src/state.cpp b/src/state.cpp
index 043d181..bf4ab0f 100644
--- a/src/state.cpp
+++ b/src/state.cpp
@@ -24,9 +24,18 @@ void from_json(const nlohmann::json &j, ExpansionState &m) {
j.at("c").get_to(m.Children);
}
+void to_json(nlohmann::json &j, const TabsState &m) {
+ j = m.Channels;
+}
+
+void from_json(const nlohmann::json &j, TabsState &m) {
+ j.get_to(m.Channels);
+}
+
void to_json(nlohmann::json &j, const AbaddonApplicationState &m) {
j["active_channel"] = m.ActiveChannel;
j["expansion"] = m.Expansion;
+ j["tabs"] = m.Tabs;
}
void from_json(const nlohmann::json &j, AbaddonApplicationState &m) {
@@ -34,4 +43,6 @@ void from_json(const nlohmann::json &j, AbaddonApplicationState &m) {
j.at("active_channel").get_to(m.ActiveChannel);
if (j.contains("expansion"))
j.at("expansion").get_to(m.Expansion);
+ if (j.contains("tabs"))
+ j.at("tabs").get_to(m.Tabs);
}
diff --git a/src/state.hpp b/src/state.hpp
index 230808f..81c36d2 100644
--- a/src/state.hpp
+++ b/src/state.hpp
@@ -1,3 +1,4 @@
+#pragma once
#include <vector>
#include <nlohmann/json.hpp>
#include "discord/snowflake.hpp"
@@ -18,9 +19,17 @@ struct ExpansionState {
friend void from_json(const nlohmann::json &j, ExpansionState &m);
};
+struct TabsState {
+ std::vector<Snowflake> Channels;
+
+ friend void to_json(nlohmann::json &j, const TabsState &m);
+ friend void from_json(const nlohmann::json &j, TabsState &m);
+};
+
struct AbaddonApplicationState {
Snowflake ActiveChannel;
ExpansionStateRoot Expansion;
+ TabsState Tabs;
friend void to_json(nlohmann::json &j, const AbaddonApplicationState &m);
friend void from_json(const nlohmann::json &j, AbaddonApplicationState &m);
diff --git a/src/windows/mainwindow.cpp b/src/windows/mainwindow.cpp
index edd485d..b77981c 100644
--- a/src/windows/mainwindow.cpp
+++ b/src/windows/mainwindow.cpp
@@ -27,6 +27,12 @@ MainWindow::MainWindow()
chat->set_hexpand(true);
chat->show();
+#ifdef WITH_LIBHANDY
+ m_channel_list.signal_action_open_new_tab().connect([this](Snowflake id) {
+ m_chat.OpenNewTab(id);
+ });
+#endif
+
m_channel_list.set_vexpand(true);
m_channel_list.set_size_request(-1, -1);
m_channel_list.show();
@@ -99,10 +105,10 @@ void MainWindow::UpdateChatWindowContents() {
m_members.UpdateMemberList();
}
-void MainWindow::UpdateChatActiveChannel(Snowflake id) {
+void MainWindow::UpdateChatActiveChannel(Snowflake id, bool expand_to) {
m_chat.SetActiveChannel(id);
m_members.SetActiveChannel(id);
- m_channel_list.SetActiveChannel(id);
+ m_channel_list.SetActiveChannel(id, expand_to);
m_content_stack.set_visible_child("chat");
}
@@ -151,6 +157,16 @@ void MainWindow::UpdateMenus() {
OnViewSubmenuPopup();
}
+#ifdef WITH_LIBHANDY
+void MainWindow::GoBack() {
+ m_chat.GoBack();
+}
+
+void MainWindow::GoForward() {
+ m_chat.GoForward();
+}
+#endif
+
void MainWindow::OnDiscordSubmenuPopup() {
auto &discord = Abaddon::Get().GetDiscordClient();
auto channel_id = GetChatActiveChannel();
@@ -236,10 +252,20 @@ void MainWindow::SetupMenu() {
m_menu_view_threads.set_label("Threads");
m_menu_view_mark_guild_as_read.set_label("Mark Server as Read");
m_menu_view_mark_guild_as_read.add_accelerator("activate", m_accels, GDK_KEY_Escape, Gdk::SHIFT_MASK, Gtk::ACCEL_VISIBLE);
+#ifdef WITH_LIBHANDY
+ m_menu_view_go_back.set_label("Go Back");
+ m_menu_view_go_forward.set_label("Go Forward");
+ m_menu_view_go_back.add_accelerator("activate", m_accels, GDK_KEY_Left, Gdk::MOD1_MASK, Gtk::ACCEL_VISIBLE);
+ m_menu_view_go_forward.add_accelerator("activate", m_accels, GDK_KEY_Right, Gdk::MOD1_MASK, Gtk::ACCEL_VISIBLE);
+#endif
m_menu_view_sub.append(m_menu_view_friends);
m_menu_view_sub.append(m_menu_view_pins);
m_menu_view_sub.append(m_menu_view_threads);
m_menu_view_sub.append(m_menu_view_mark_guild_as_read);
+#ifdef WITH_LIBHANDY
+ m_menu_view_sub.append(m_menu_view_go_back);
+ m_menu_view_sub.append(m_menu_view_go_forward);
+#endif
m_menu_bar.append(m_menu_file);
m_menu_bar.append(m_menu_discord);
@@ -279,7 +305,7 @@ void MainWindow::SetupMenu() {
});
m_menu_view_friends.signal_activate().connect([this] {
- UpdateChatActiveChannel(Snowflake::Invalid);
+ UpdateChatActiveChannel(Snowflake::Invalid, true);
m_members.UpdateMemberList();
m_content_stack.set_visible_child("friends");
});
@@ -300,6 +326,16 @@ void MainWindow::SetupMenu() {
discord.MarkGuildAsRead(*channel->GuildID, NOOP_CALLBACK);
}
});
+
+#ifdef WITH_LIBHANDY
+ m_menu_view_go_back.signal_activate().connect([this] {
+ GoBack();
+ });
+
+ m_menu_view_go_forward.signal_activate().connect([this] {
+ GoForward();
+ });
+#endif
}
MainWindow::type_signal_action_connect MainWindow::signal_action_connect() {
diff --git a/src/windows/mainwindow.hpp b/src/windows/mainwindow.hpp
index 0932af5..ce3a576 100644
--- a/src/windows/mainwindow.hpp
+++ b/src/windows/mainwindow.hpp
@@ -13,7 +13,7 @@ public:
void UpdateMembers();
void UpdateChannelListing();
void UpdateChatWindowContents();
- void UpdateChatActiveChannel(Snowflake id);
+ void UpdateChatActiveChannel(Snowflake id, bool expand_to);
Snowflake GetChatActiveChannel() const;
void UpdateChatNewMessage(const Message &data);
void UpdateChatMessageDeleted(Snowflake id, Snowflake channel_id);
@@ -25,6 +25,11 @@ public:
void UpdateChatReactionRemove(Snowflake id, const Glib::ustring &param);
void UpdateMenus();
+#ifdef WITH_LIBHANDY
+ void GoBack();
+ void GoForward();
+#endif
+
ChannelList *GetChannelList();
ChatWindow *GetChatWindow();
MemberList *GetMemberList();
@@ -68,6 +73,10 @@ private:
Gtk::MenuItem m_menu_view_pins;
Gtk::MenuItem m_menu_view_threads;
Gtk::MenuItem m_menu_view_mark_guild_as_read;
+#ifdef WITH_LIBHANDY
+ Gtk::MenuItem m_menu_view_go_back;
+ Gtk::MenuItem m_menu_view_go_forward;
+#endif
void OnViewSubmenuPopup();
public: