mirror of
https://github.com/ZoiteChat/zoitechat.git
synced 2026-07-17 00:49:25 +00:00
Compare commits
88 Commits
2d12da79b0
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2db3076c92 | ||
| 90daed18df | |||
|
|
65dd2c645a | ||
| 89d70a940c | |||
|
|
4944517a0e | ||
| 2a7ee7f0b2 | |||
|
|
6edbd47c9a | ||
| d755c39e4a | |||
|
|
b7f8327209 | ||
| 27c82d8411 | |||
| e0b159c069 | |||
|
|
cfb19820fa | ||
|
|
e05ca13608 | ||
|
|
15cf96c01f | ||
| 2caec91a0f | |||
| 9465a1e0a8 | |||
| 251272c708 | |||
| f068a0e0fa | |||
| d0034cd112 | |||
| ae6d5ebeef | |||
|
|
8466e70424 | ||
| c3d8f27f48 | |||
| b85229f1a1 | |||
|
|
d03298a048 | ||
|
|
798e0b487e | ||
| 49444d72b4 | |||
|
|
c0e0fe4dcb | ||
|
|
9a189848ea | ||
| 96b09bfc1d | |||
| 9a6c913d89 | |||
| 42010594be | |||
| 5469c30b75 | |||
| fe9a2ee202 | |||
|
|
5e27dc7e9f | ||
|
|
69d083e3c0 | ||
| 2b95f9724e | |||
| 4066a07892 | |||
|
|
443ab58f65 | ||
| e0116acbe4 | |||
|
|
91e9bf0207 | ||
| aff0335f7e | |||
|
|
4d8baaa19b | ||
| 65452d80f1 | |||
|
|
df4d432151 | ||
|
|
1390f2c783 | ||
| be99d0e900 | |||
| 97fcf1d5fc | |||
| c9b14d7c45 | |||
| 3d0dda0347 | |||
|
|
8d69b35d67 | ||
| 7ba142a9f2 | |||
|
|
56ac55806d | ||
| 47f53c14db | |||
| a47d40b914 | |||
| be33ae30bb | |||
| 6d5f1306bf | |||
| ad3dc716e7 | |||
|
|
089b125a89 | ||
| dfd4554a11 | |||
| 1b98297881 | |||
|
|
cacc8fcafe | ||
| ac07d39db8 | |||
|
|
e27aa77cec | ||
| cbc9f68dd6 | |||
| 944d0f5a70 | |||
| 9ded7ef830 | |||
| adb18b7ce3 | |||
|
|
8220e60257 | ||
| b09af655e7 | |||
| 5833f8a3d8 | |||
|
|
72132d5e88 | ||
|
|
4167c4db78 | ||
| 3e1f6b9137 | |||
|
|
bb08b19c0d | ||
|
|
7497919e8f | ||
|
|
41cbe7b31c | ||
| 425e951341 | |||
| 2e4a0b92fc | |||
| 9f839579e2 | |||
| 900066b9d4 | |||
| 97534b0670 | |||
|
|
62672ade04 | ||
| b22e1c1ccc | |||
|
|
1b8e60c26d | ||
| 035dfdd332 | |||
| d1707d3c72 | |||
|
|
c9bed097ba | ||
| 5acb90025f |
132
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
132
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,132 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report a problem with ZoiteChat
|
||||
title: "[Bug]: "
|
||||
labels: bug
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
```
|
||||
+------------------------------------------------------------+
|
||||
| ZoiteChat Bug Report |
|
||||
| Please fill this out so the bug goblin has enough crumbs. |
|
||||
+------------------------------------------------------------+
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
What went wrong?
|
||||
|
||||
```text
|
||||
Describe the bug clearly and briefly.
|
||||
```
|
||||
|
||||
## Steps to Reproduce
|
||||
|
||||
```text
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
```
|
||||
|
||||
## Expected Result
|
||||
|
||||
```text
|
||||
What should have happened?
|
||||
```
|
||||
|
||||
## Actual Result
|
||||
|
||||
```text
|
||||
What happened instead?
|
||||
```
|
||||
|
||||
## ZoiteChat Version
|
||||
|
||||
```text
|
||||
ZoiteChat version:
|
||||
Build/source: GitHub release / Fedora package / source build / Windows installer / other
|
||||
Commit hash, if built from source:
|
||||
```
|
||||
|
||||
## System Information
|
||||
|
||||
```text
|
||||
OS:
|
||||
OS version:
|
||||
Desktop environment:
|
||||
Display server: Wayland / X11 / Windows / other
|
||||
CPU architecture: x86_64 / aarch64 / other
|
||||
```
|
||||
|
||||
## IRC / Network Details
|
||||
|
||||
```text
|
||||
IRC network/server:
|
||||
Port:
|
||||
TLS/SSL enabled: yes / no
|
||||
SASL enabled: yes / no
|
||||
Using bouncer/ZNC: yes / no
|
||||
Proxy/VPN involved: yes / no
|
||||
IRCv3 capabilities involved, if known:
|
||||
```
|
||||
|
||||
## Plugins, Scripts, and Config
|
||||
|
||||
```text
|
||||
Plugins enabled:
|
||||
Scripts loaded:
|
||||
Theme/custom CSS:
|
||||
Custom config changes:
|
||||
Does the bug still happen with a clean config? yes / no / untested
|
||||
```
|
||||
|
||||
## Logs / Terminal Output
|
||||
|
||||
Paste relevant output below.
|
||||
|
||||
```text
|
||||
```
|
||||
|
||||
## Raw IRC Lines, If Relevant
|
||||
|
||||
Remove passwords, tokens, IPs, private channels, private messages, certs, and account details before posting.
|
||||
|
||||
```text
|
||||
```
|
||||
|
||||
## Crash Details, If Relevant
|
||||
|
||||
```text
|
||||
Did ZoiteChat crash? yes / no
|
||||
Crash reporter output:
|
||||
Backtrace:
|
||||
Core dump available: yes / no
|
||||
```
|
||||
|
||||
## Screenshots
|
||||
|
||||
Attach screenshots or screen recordings if the issue is visual.
|
||||
|
||||
## Regression Check
|
||||
|
||||
```text
|
||||
Did this work in an older version? yes / no / unknown
|
||||
Last known working version:
|
||||
First broken version:
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
* [ ] I searched existing issues first.
|
||||
* [ ] I tested with the latest ZoiteChat version available to me.
|
||||
* [ ] I included steps to reproduce the issue.
|
||||
* [ ] I removed private information from logs and raw IRC lines.
|
||||
* [ ] I included system and network details where relevant.
|
||||
|
||||
## Extra Notes
|
||||
|
||||
```text
|
||||
Anything else that might help.
|
||||
```
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
57
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
57
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an improvement for ZoiteChat
|
||||
title: "[Feature]: "
|
||||
labels: enhancement
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
```text
|
||||
Describe the feature or enhancement clearly and briefly.
|
||||
```
|
||||
|
||||
## Problem
|
||||
|
||||
```text
|
||||
What problem would this solve?
|
||||
```
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
```text
|
||||
How should ZoiteChat behave?
|
||||
```
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
```text
|
||||
Other approaches or workarounds you considered.
|
||||
```
|
||||
|
||||
## Use Case
|
||||
|
||||
```text
|
||||
Who benefits from this, and when would they use it?
|
||||
```
|
||||
|
||||
## Platform Details
|
||||
|
||||
```text
|
||||
OS:
|
||||
ZoiteChat version:
|
||||
Build/source: GitHub release / Fedora package / source build / Windows installer / other
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
* [ ] I searched existing issues first.
|
||||
* [ ] I checked whether this already exists in the latest ZoiteChat version available to me.
|
||||
* [ ] I explained the problem this would solve.
|
||||
|
||||
## Extra Notes
|
||||
|
||||
```text
|
||||
Anything else that might help.
|
||||
```
|
||||
79
.github/workflows/manjaro-package-build.yml
vendored
79
.github/workflows/manjaro-package-build.yml
vendored
@@ -1,79 +0,0 @@
|
||||
name: Manjaro Package Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, main]
|
||||
pull_request:
|
||||
branches: [master, main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
manjaro-package:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: manjarolinux/base:latest
|
||||
|
||||
steps:
|
||||
- name: Install packaging toolchain
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
pacman -Syu --noconfirm
|
||||
pacman -S --noconfirm --needed \
|
||||
base-devel \
|
||||
git \
|
||||
namcap \
|
||||
meson \
|
||||
ninja \
|
||||
pkgconf \
|
||||
glib2 \
|
||||
glib2-devel \
|
||||
gtk3 \
|
||||
openssl \
|
||||
libcanberra \
|
||||
libayatana-appindicator \
|
||||
iso-codes \
|
||||
lua \
|
||||
perl \
|
||||
python \
|
||||
python-cffi \
|
||||
pciutils
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
|
||||
- name: Build package and generate .SRCINFO (non-root)
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
id -u builder >/dev/null 2>&1 || useradd -m builder
|
||||
chown -R builder:builder "$GITHUB_WORKSPACE"
|
||||
su builder -c "cd '$GITHUB_WORKSPACE/packaging/manjaro' && makepkg --noconfirm --syncdeps --cleanbuild"
|
||||
su builder -c "cd '$GITHUB_WORKSPACE/packaging/manjaro' && makepkg --printsrcinfo > .SRCINFO"
|
||||
|
||||
- name: Lint PKGBUILD and package with namcap
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
cd "$GITHUB_WORKSPACE/packaging/manjaro"
|
||||
namcap PKGBUILD
|
||||
pkg_file="$(find . -maxdepth 1 -type f -name '*.pkg.tar.*' | head -n1)"
|
||||
namcap "$pkg_file"
|
||||
|
||||
- name: Collect artifacts
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
mkdir -p artifacts
|
||||
cp -v "$GITHUB_WORKSPACE"/packaging/manjaro/*.pkg.tar.* artifacts/
|
||||
cp -v "$GITHUB_WORKSPACE"/packaging/manjaro/PKGBUILD artifacts/
|
||||
cp -v "$GITHUB_WORKSPACE"/packaging/manjaro/.SRCINFO artifacts/
|
||||
|
||||
- name: Upload package artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: zoitechat-manjaro-package
|
||||
path: artifacts/*
|
||||
if-no-files-found: error
|
||||
41
.github/workflows/windows-build.yml
vendored
41
.github/workflows/windows-build.yml
vendored
@@ -113,6 +113,26 @@ jobs:
|
||||
New-Item -Path $pyDir -Name "${{ matrix.platform }}" -ItemType Junction -Value $pyRoot | Out-Null
|
||||
}
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.8.10'
|
||||
architecture: ${{ matrix.arch }}
|
||||
|
||||
- name: Prepare Python 3.8
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install cffi
|
||||
|
||||
$pyRoot = $env:pythonLocation
|
||||
if (-not $pyRoot) { $pyRoot = & python -c "import sys; print(sys.prefix)" }
|
||||
|
||||
foreach ($pyDir in @("C:\gtk-build\python-3.8.10", "C:\gtk-build\python-3.8")) {
|
||||
New-Item -Path $pyDir -ItemType Directory -Force | Out-Null
|
||||
$target = Join-Path $pyDir "${{ matrix.platform }}"
|
||||
if (Test-Path $target) { Remove-Item $target -Recurse -Force }
|
||||
New-Item -Path $pyDir -Name "${{ matrix.platform }}" -ItemType Junction -Value $pyRoot | Out-Null
|
||||
}
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat"
|
||||
@@ -124,12 +144,33 @@ jobs:
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
set "PYTHON38_DIR=C:\gtk-build\python-3.8\${{ matrix.platform }}"
|
||||
if not exist "%PYTHON38_DIR%\libs\python38.lib" (
|
||||
echo Missing %PYTHON38_DIR%\libs\python38.lib
|
||||
dir "%PYTHON38_DIR%\libs"
|
||||
exit /b 1
|
||||
)
|
||||
if not exist "%PYTHON38_DIR%\Lib\site-packages\_cffi_backend.cp38*.pyd" (
|
||||
echo Missing %PYTHON38_DIR%\Lib\site-packages\_cffi_backend.cp38*.pyd
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
set "LIB=%PYTHON_DIR%\libs;%LIB%"
|
||||
set "INCLUDE=%PYTHON_DIR%\include;%INCLUDE%"
|
||||
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "$archiveLib='C:\gtk-build\gtk\x64\release\lib\libarchive.lib'; if (-not (Test-Path $archiveLib)) { $archiveDll = Get-ChildItem 'C:\gtk-build\gtk\x64\release\bin\libarchive*.dll' | Select-Object -First 1; if ($archiveDll) { Push-Location 'C:\gtk-build\gtk\x64\release\lib'; & 'C:\gtk-build\gendef\gendef.exe' $archiveDll.FullName | Out-Null; $archiveDef = Get-ChildItem 'libarchive*.def' | Select-Object -First 1; if ($archiveDef) { & lib /def:$archiveDef.Name /machine:${{ matrix.platform }} /out:libarchive.lib | Out-Null }; Pop-Location } }"
|
||||
|
||||
msbuild win32\zoitechat.sln /m /verbosity:minimal /p:Configuration=Release /p:Platform=${{ matrix.platform }}
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
if not exist "..\zoitechat-build\${{ matrix.platform }}\rel\plugins\hcpython38.dll" (
|
||||
echo hcpython38.dll was not built
|
||||
exit /b 1
|
||||
)
|
||||
if not exist "..\zoitechat-build\${{ matrix.platform }}\rel\_cffi_backend.cp38*.pyd" (
|
||||
echo _cffi_backend for Python 3.8 was not staged for the installer
|
||||
exit /b 1
|
||||
)
|
||||
shell: cmd
|
||||
|
||||
- name: Preparing Artifacts
|
||||
|
||||
46
COPYING
46
COPYING
@@ -1,12 +1,12 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||
51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
<https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
@@ -15,7 +15,7 @@ software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Library General Public License instead.) You can apply it to
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
@@ -55,8 +55,8 @@ patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
@@ -110,7 +110,7 @@ above, provided that you also meet all of these conditions:
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
@@ -168,7 +168,7 @@ access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
@@ -225,7 +225,7 @@ impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
@@ -255,7 +255,7 @@ make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
@@ -277,9 +277,9 @@ YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
@@ -291,7 +291,7 @@ convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) 19yy <name of author>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -303,17 +303,15 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) 19yy name of author
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
@@ -330,11 +328,11 @@ necessary. Here is a sample; alter the names:
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
<signature of Moe Ghoul>, 1 April 1989
|
||||
Moe Ghoul, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Library General
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
|
||||
@@ -1,6 +1,35 @@
|
||||
ZoiteChat ChangeLog
|
||||
=================
|
||||
|
||||
2.18.3 (2026-06-29)
|
||||
-------------------
|
||||
|
||||
- Added IRCv3 support for message tags, echo-message, typing notifications, and replies.
|
||||
- Restored the accept-invalid-cert TLS bypass behavior.
|
||||
- Replaced legacy IPv4 APIs and added support for OpenSSL 4 APIs.
|
||||
- Snapshot Python hooks during plugin unload for safer plugin shutdown.
|
||||
- Fixed Windows GtkStatusIcon tray menu popup behavior.
|
||||
- Fixed self-echoed private messages routing to the target tab.
|
||||
- Fixed the lag ping timeout window.
|
||||
|
||||
2.18.2 (2026-06-20)
|
||||
-------------------
|
||||
|
||||
- Improved security and connection handling with stronger TLS defaults,
|
||||
safer certificate/hostname validation, FiSHLiM OpenSSL fixes, keyring support,
|
||||
and getaddrinfo-based DCC lookups.
|
||||
- Expanded FiSHLiM usability with a GTK key manager, context-menu access, and clearer
|
||||
channel/private-message key handling.
|
||||
- Improved keyboard and navigation behavior with customizable keybinds, reset support,
|
||||
channel-name switching, /server -noproxy, and configurable stale-link ping checks.
|
||||
- Refined the GTK interface with native file chooser dialogs, topic layout preferences,
|
||||
timestamp/date hover tooltips, better restore-down relayout, text scroll speed controls,
|
||||
and aligned network meters.
|
||||
- Added privacy and display options, including hiding join/part hostmasks and improved
|
||||
topic/multiline mode layout controls.
|
||||
- Fixed several platform/UI edge cases, including tray restore from iconified state, DBus unload
|
||||
handling, GTK3 theme refresh/saving, and tab switcher compatibility.
|
||||
|
||||
2.18.1 (2026-05-21)
|
||||
-------------------
|
||||
|
||||
|
||||
@@ -29,6 +29,42 @@
|
||||
<id>zoitechat.desktop</id>
|
||||
</provides>
|
||||
<releases>
|
||||
<release date="2026-06-29" version="2.18.3">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Added IRCv3 support for message tags, echo-message, typing notifications, and replies.</li>
|
||||
<li>Restored the accept-invalid-cert TLS bypass behavior.</li>
|
||||
<li>Replaced legacy IPv4 APIs and added support for OpenSSL 4 APIs.</li>
|
||||
<li>Snapshot Python hooks during plugin unload for safer plugin shutdown.</li>
|
||||
<li>Fixed Windows GtkStatusIcon tray menu popup behavior.</li>
|
||||
<li>Fixed self-echoed private messages routing to the target tab.</li>
|
||||
<li>Fixed the lag ping timeout window.</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release date="2026-06-20" version="2.18.2">
|
||||
<description>
|
||||
<p>Security and connections:</p>
|
||||
<ul>
|
||||
<li>Improved TLS, certificate and hostname validation, FiSHLiM OpenSSL handling, keyring support, and DCC host lookups.</li>
|
||||
</ul>
|
||||
<p>FiSHLiM:</p>
|
||||
<ul>
|
||||
<li>Added a GTK key manager with context-menu access and clearer channel/private-message key handling.</li>
|
||||
</ul>
|
||||
<p>Interface and preferences:</p>
|
||||
<ul>
|
||||
<li>Improved keybind customization, reset support, channel-name switching, <code>/server -noproxy</code>, and stale-link ping checks.</li>
|
||||
<li>Refined GTK behavior with native file choosers, topic layout preferences, timestamp/date tooltips, text scroll controls, and aligned network meters.</li>
|
||||
<li>Added privacy and display options for hiding join/part hostmasks and controlling topic/multiline layout.</li>
|
||||
</ul>
|
||||
<p>Fixes:</p>
|
||||
<ul>
|
||||
<li>Fixed tray restore, DBus unload handling, GTK3 theme refresh/saving, topic relayout, and tab switcher compatibility.</li>
|
||||
<li>Updated project documentation.</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release date="2026-05-21" version="2.18.1">
|
||||
<description>
|
||||
<ul>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
project('zoitechat', 'c',
|
||||
version: '2.18.1',
|
||||
version: '2.18.3',
|
||||
meson_version: '>= 0.55.0',
|
||||
default_options: [
|
||||
'c_std=c17',
|
||||
|
||||
102
packaging/fedora/zoitechat.spec
Normal file
102
packaging/fedora/zoitechat.spec
Normal file
@@ -0,0 +1,102 @@
|
||||
Name: zoitechat
|
||||
Version: 2.18.3
|
||||
Release: %autorelease
|
||||
Summary: HexChat-based IRC client
|
||||
License: GPL-2.0-or-later WITH cryptsetup-OpenSSL-exception
|
||||
URL: https://github.com/ZoiteChat/zoitechat
|
||||
Source0: %{url}/archive/refs/tags/%{name}-%{version}.tar.gz
|
||||
|
||||
BuildRequires: desktop-file-utils
|
||||
BuildRequires: gcc
|
||||
BuildRequires: gettext
|
||||
BuildRequires: libappstream-glib
|
||||
BuildRequires: meson >= 0.55.0
|
||||
BuildRequires: perl
|
||||
BuildRequires: perl-devel
|
||||
BuildRequires: python3
|
||||
BuildRequires: python3-cffi
|
||||
BuildRequires: publicsuffix-list
|
||||
BuildRequires: xwayland-run
|
||||
BuildRequires: weston
|
||||
BuildRequires: pkgconfig(ayatana-appindicator3-0.1)
|
||||
BuildRequires: pkgconfig(dbus-glib-1)
|
||||
BuildRequires: pkgconfig(gio-2.0) >= 2.36.0
|
||||
BuildRequires: pkgconfig(gmodule-2.0)
|
||||
BuildRequires: pkgconfig(gtk+-3.0) >= 3.22
|
||||
BuildRequires: pkgconfig(iso-codes)
|
||||
BuildRequires: pkgconfig(libarchive)
|
||||
BuildRequires: pkgconfig(libcanberra) >= 0.22
|
||||
BuildRequires: pkgconfig(libpci)
|
||||
BuildRequires: pkgconfig(lua)
|
||||
BuildRequires: pkgconfig(openssl) >= 0.9.8
|
||||
BuildRequires: pkgconfig(python3)
|
||||
|
||||
Requires: hicolor-icon-theme
|
||||
Requires: iso-codes
|
||||
|
||||
%package devel
|
||||
Summary: Development files for ZoiteChat plugins
|
||||
Requires: %{name}%{?_isa} = %{version}-%{release}
|
||||
|
||||
%description devel
|
||||
Development files for building ZoiteChat plugins.
|
||||
|
||||
%description
|
||||
ZoiteChat is a HexChat-based IRC client for Windows and UNIX-like operating
|
||||
systems.
|
||||
|
||||
%prep
|
||||
%autosetup -C
|
||||
|
||||
%build
|
||||
%meson \
|
||||
-Dtext-frontend=false \
|
||||
-Dwith-checksum=true \
|
||||
-Dwith-fishlim=true \
|
||||
-Dwith-lua=lua \
|
||||
-Dwith-perl=perl \
|
||||
-Dwith-python=python3 \
|
||||
-Dwith-sysinfo=true \
|
||||
-Dinstall-appdata=true \
|
||||
-Dinstall-plugin-metainfo=true
|
||||
%meson_build
|
||||
|
||||
%install
|
||||
%meson_install
|
||||
%find_lang %{name}
|
||||
|
||||
%check
|
||||
desktop-file-validate %{buildroot}%{_datadir}/applications/net.zoite.Zoitechat.desktop
|
||||
|
||||
appstream-util validate-relax --nonet %{buildroot}%{_datadir}/metainfo/net.zoite.Zoitechat.appdata.xml
|
||||
appstream-util validate-relax --nonet %{buildroot}%{_datadir}/metainfo/net.zoite.Zoitechat*.metainfo.xml
|
||||
|
||||
xwfb-run -- /usr/bin/meson test -C %{_vpath_builddir} --num-processes %{_smp_build_ncpus} --print-errorlogs \
|
||||
"Theme Manager Dispatch Routing Tests" \
|
||||
"Validate net.zoite.Zoitechat.desktop" \
|
||||
"Validate translations" \
|
||||
"Fishlim Tests"
|
||||
|
||||
%files -f %{name}.lang
|
||||
%license COPYING
|
||||
%doc readme.md troubleshooting.md
|
||||
%{_bindir}/zoitechat
|
||||
%{_datadir}/applications/net.zoite.Zoitechat.desktop
|
||||
%{_datadir}/dbus-1/services/org.zoitechat.service.service
|
||||
%{_datadir}/icons/hicolor/48x48/apps/net.zoite.Zoitechat.png
|
||||
%{_datadir}/icons/hicolor/scalable/apps/net.zoite.Zoitechat.svg
|
||||
%{_datadir}/metainfo/net.zoite.Zoitechat.appdata.xml
|
||||
%{_datadir}/metainfo/net.zoite.Zoitechat*.metainfo.xml
|
||||
%dir %{_libdir}/zoitechat
|
||||
%dir %{_libdir}/zoitechat/plugins
|
||||
%dir %{_libdir}/zoitechat/python
|
||||
%{_libdir}/zoitechat/plugins/*.so
|
||||
%{_libdir}/zoitechat/python/*.py
|
||||
%{_mandir}/man1/zoitechat.1*
|
||||
|
||||
%files devel
|
||||
%{_includedir}/zoitechat-plugin.h
|
||||
%{_libdir}/pkgconfig/zoitechat-plugin.pc
|
||||
|
||||
%changelog
|
||||
%autochangelog
|
||||
@@ -1,69 +0,0 @@
|
||||
# Maintainer: ZoiteChat Maintainers <zoitechat@users.noreply.github.com>
|
||||
|
||||
pkgname=zoitechat-git
|
||||
pkgver=0
|
||||
pkgrel=1
|
||||
pkgdesc='IRC client for GNOME (ZoiteChat development snapshot)'
|
||||
arch=('x86_64')
|
||||
url='https://github.com/zoitechat/zoitechat'
|
||||
license=('GPL-2.0-or-later')
|
||||
depends=(
|
||||
'glib2'
|
||||
'gtk3'
|
||||
'iso-codes'
|
||||
'libayatana-appindicator'
|
||||
'libcanberra'
|
||||
'lua'
|
||||
'openssl'
|
||||
'perl'
|
||||
'python-cffi'
|
||||
)
|
||||
makedepends=(
|
||||
'git'
|
||||
'meson'
|
||||
'ninja'
|
||||
'pkgconf'
|
||||
'python'
|
||||
)
|
||||
optdepends=(
|
||||
'pciutils: sysinfo plugin hardware detection details'
|
||||
)
|
||||
provides=('zoitechat')
|
||||
conflicts=('zoitechat')
|
||||
|
||||
_repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
source=("zoitechat::git+file://${_repo_root}")
|
||||
sha256sums=('SKIP')
|
||||
|
||||
pkgver() {
|
||||
cd "$srcdir/zoitechat"
|
||||
|
||||
if git describe --long --tags --abbrev=7 >/dev/null 2>&1; then
|
||||
git describe --long --tags --abbrev=7 \
|
||||
| sed 's/^v//' \
|
||||
| sed -E 's/([^-]+)-([0-9]+)-g/\1.r\2.g/' \
|
||||
| sed 's/-/./g'
|
||||
else
|
||||
printf '0.r%s.%s\n' \
|
||||
"$(git rev-list --count HEAD)" \
|
||||
"$(git rev-parse --short=7 HEAD)"
|
||||
fi
|
||||
}
|
||||
|
||||
build() {
|
||||
arch-meson zoitechat build \
|
||||
-Dtext-frontend=false \
|
||||
-Dwith-checksum=true \
|
||||
-Dwith-fishlim=true \
|
||||
-Dwith-lua=lua \
|
||||
-Dwith-perl=perl \
|
||||
-Dwith-python=python3 \
|
||||
-Dwith-sysinfo=true \
|
||||
-Dinstall-plugin-metainfo=true
|
||||
|
||||
meson compile -C build
|
||||
}
|
||||
|
||||
package() {
|
||||
meson install -C build --destdir "$pkgdir"
|
||||
}
|
||||
@@ -26,7 +26,7 @@
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>WIN32;_WIN64;_AMD64_;NDEBUG;_WINDOWS;_USRDLL;FISHLIM_EXPORTS;HAVE_DH_SET0_PQG;HAVE_DH_GET0_KEY;HAVE_DH_SET0_KEY;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<AdditionalIncludeDirectories>$(DepsRoot)\include;$(OpenSslInclude);$(Glib);..\..\src\common;$(ZoiteChatLib);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<AdditionalIncludeDirectories>$(DepsRoot)\include;$(OpenSslInclude);$(Glib);$(Gtk);..\..\src\common;$(ZoiteChatLib);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<ModuleDefinitionFile>fishlim.def</ModuleDefinitionFile>
|
||||
|
||||
@@ -194,6 +194,18 @@ static gboolean keyfile_save_to_file (GKeyFile *keyfile, char *filename) {
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
gchar **keystore_get_targets(gsize *length) {
|
||||
GKeyFile *keyfile;
|
||||
gchar **groups;
|
||||
|
||||
keyfile = getConfigFile();
|
||||
groups = g_key_file_get_groups(keyfile, length);
|
||||
g_key_file_free(keyfile);
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the key store file to disk.
|
||||
*/
|
||||
@@ -217,6 +229,7 @@ G_GNUC_END_IGNORE_DEPRECATIONS
|
||||
/**
|
||||
* Sets a key in the key store file.
|
||||
*/
|
||||
|
||||
gboolean keystore_store_key(const char *nick, const char *key, enum fish_mode mode) {
|
||||
const char *password;
|
||||
char *encrypted;
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
char *keystore_get_key(const char *nick, enum fish_mode *mode);
|
||||
gboolean keystore_store_key(const char *nick, const char *key, enum fish_mode mode);
|
||||
gboolean keystore_delete_nick(const char *nick);
|
||||
gchar **keystore_get_targets(gsize *length);
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ fishlim_sources = [
|
||||
]
|
||||
|
||||
shared_module('fishlim', fishlim_sources,
|
||||
dependencies: [libgio_dep, zoitechat_plugin_dep, libssl_dep],
|
||||
dependencies: [libgio_dep, gtk_dep, zoitechat_plugin_dep, libssl_dep],
|
||||
c_args: ['-DOPENSSL_API_COMPAT=0x10100000L'],
|
||||
install: true,
|
||||
install_dir: plugindir,
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "config.h"
|
||||
|
||||
#include <glib.h>
|
||||
#include <gtk/gtk.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "zoitechat-plugin.h"
|
||||
@@ -44,7 +45,7 @@ static const char plugin_version[] = "1.0.0";
|
||||
|
||||
static const char usage_setkey[] = "Usage: SETKEY [<nick or #channel>] [<mode>:]<password>, sets the key for a channel or nick. Modes: ECB, CBC";
|
||||
static const char usage_delkey[] = "Usage: DELKEY [<nick or #channel>], deletes the key for a channel or nick";
|
||||
static const char usage_keyx[] = "Usage: KEYX [<nick>], performs DH1080 key-exchange with <nick>";
|
||||
static const char usage_keyx[] = "Usage: KEYX [<nick>] [ECB|CBC], performs DH1080 key-exchange with <nick>";
|
||||
static const char usage_topic[] = "Usage: TOPIC+ <topic>, sets a new encrypted topic for the current channel";
|
||||
static const char usage_notice[] = "Usage: NOTICE+ <nick or #channel> <notice>";
|
||||
static const char usage_msg[] = "Usage: MSG+ <nick or #channel> <message>";
|
||||
@@ -52,6 +53,13 @@ static const char usage_msg[] = "Usage: MSG+ <nick or #channel> <message>";
|
||||
|
||||
static zoitechat_plugin *ph;
|
||||
static GHashTable *pending_exchanges;
|
||||
static GtkWidget *fishlim_dialog;
|
||||
static GtkWidget *fishlim_target_entry;
|
||||
static GtkWidget *fishlim_key_entry;
|
||||
static GtkWidget *fishlim_mode_combo;
|
||||
static GtkWidget *fishlim_status_label;
|
||||
static GtkListStore *fishlim_store;
|
||||
static GtkWidget *fishlim_view;
|
||||
|
||||
|
||||
/**
|
||||
@@ -455,6 +463,314 @@ cleanup:
|
||||
return ZOITECHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
|
||||
static const char *fishlim_gui_target(void) {
|
||||
const char *target;
|
||||
|
||||
target = gtk_entry_get_text(GTK_ENTRY(fishlim_target_entry));
|
||||
if (target && *target)
|
||||
return target;
|
||||
|
||||
return zoitechat_get_info(ph, "channel");
|
||||
}
|
||||
|
||||
static void fishlim_gui_refresh(void) {
|
||||
GtkTreeIter iter;
|
||||
gchar **targets;
|
||||
gsize length, i;
|
||||
const char *target;
|
||||
char *key;
|
||||
enum fish_mode mode;
|
||||
|
||||
if (!fishlim_dialog)
|
||||
return;
|
||||
|
||||
gtk_list_store_clear(fishlim_store);
|
||||
targets = keystore_get_targets(&length);
|
||||
|
||||
for (i = 0; targets && i < length; i++) {
|
||||
key = keystore_get_key(targets[i], &mode);
|
||||
gtk_list_store_append(fishlim_store, &iter);
|
||||
gtk_list_store_set(fishlim_store, &iter, 0, targets[i], 1, fish_modes[mode], 2, key ? "Stored" : "Unreadable", -1);
|
||||
g_free(key);
|
||||
}
|
||||
|
||||
g_strfreev(targets);
|
||||
|
||||
target = fishlim_gui_target();
|
||||
key = target ? keystore_get_key(target, &mode) : NULL;
|
||||
|
||||
if (key) {
|
||||
char *text = g_strdup_printf("Key stored for %s (%s)", target, fish_modes[mode]);
|
||||
gtk_label_set_text(GTK_LABEL(fishlim_status_label), text);
|
||||
gtk_combo_box_set_active(GTK_COMBO_BOX(fishlim_mode_combo), mode == FISH_CBC_MODE ? 1 : 0);
|
||||
g_free(text);
|
||||
g_free(key);
|
||||
} else if (target && *target) {
|
||||
char *text = g_strdup_printf("No key for %s", target);
|
||||
gtk_label_set_text(GTK_LABEL(fishlim_status_label), text);
|
||||
g_free(text);
|
||||
} else {
|
||||
gtk_label_set_text(GTK_LABEL(fishlim_status_label), "No target selected");
|
||||
}
|
||||
}
|
||||
|
||||
static void fishlim_gui_refresh_cb(GtkWidget *widget, gpointer data) {
|
||||
fishlim_gui_refresh();
|
||||
}
|
||||
|
||||
static void fishlim_gui_send_channel_key(const char *channel, const char *mode, const char *key) {
|
||||
GtkWidget *dialog, *content, *entry, *label;
|
||||
char **users;
|
||||
int i;
|
||||
|
||||
if (!channel || !*channel || irc_is_query(channel))
|
||||
return;
|
||||
|
||||
dialog = gtk_dialog_new_with_buttons("Share Channel Key", GTK_WINDOW(fishlim_dialog), GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, "_Skip", GTK_RESPONSE_CANCEL, "_Send", GTK_RESPONSE_OK, NULL);
|
||||
content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
|
||||
label = gtk_label_new("Send this channel key to users by private message. Enter users separated by commas.");
|
||||
gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
|
||||
gtk_box_pack_start(GTK_BOX(content), label, FALSE, FALSE, 6);
|
||||
entry = gtk_entry_new();
|
||||
gtk_entry_set_placeholder_text(GTK_ENTRY(entry), "nick1, nick2, nick3");
|
||||
gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);
|
||||
gtk_box_pack_start(GTK_BOX(content), entry, FALSE, FALSE, 6);
|
||||
gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_OK);
|
||||
gtk_widget_show_all(dialog);
|
||||
|
||||
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_OK) {
|
||||
users = g_strsplit(gtk_entry_get_text(GTK_ENTRY(entry)), ",", -1);
|
||||
for (i = 0; users && users[i]; i++) {
|
||||
char *user = g_strstrip(users[i]);
|
||||
if (*user)
|
||||
zoitechat_commandf(ph, "msg %s Encrypted chat invite: join %s, open FiSHLiM Key Manager, add channel %s with %s key: %s. Key exchange is for private messages only.", user, channel, channel, mode, key);
|
||||
}
|
||||
g_strfreev(users);
|
||||
}
|
||||
|
||||
gtk_widget_destroy(dialog);
|
||||
}
|
||||
|
||||
static void fishlim_gui_set(GtkWidget *widget, gpointer data) {
|
||||
const char *target = fishlim_gui_target();
|
||||
const char *key = gtk_entry_get_text(GTK_ENTRY(fishlim_key_entry));
|
||||
const char *mode = gtk_combo_box_get_active(GTK_COMBO_BOX(fishlim_mode_combo)) == 1 ? "CBC" : "ECB";
|
||||
|
||||
if (!target || !*target || !key || !*key) {
|
||||
zoitechat_printf(ph, "%s\n", usage_setkey);
|
||||
return;
|
||||
}
|
||||
|
||||
zoitechat_commandf(ph, "SETKEY %s %s:%s", target, mode, key);
|
||||
fishlim_gui_send_channel_key(target, mode, key);
|
||||
gtk_entry_set_text(GTK_ENTRY(fishlim_key_entry), "");
|
||||
fishlim_gui_refresh();
|
||||
}
|
||||
|
||||
static gboolean fishlim_gui_confirm_delete(const char *target) {
|
||||
GtkWidget *dialog;
|
||||
char *message;
|
||||
gboolean confirmed;
|
||||
|
||||
message = g_strdup_printf("Delete the key for %s?", target);
|
||||
dialog = gtk_message_dialog_new(GTK_WINDOW(fishlim_dialog), GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_WARNING, GTK_BUTTONS_NONE, "%s", message);
|
||||
gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog), "This cannot be undone.");
|
||||
gtk_dialog_add_buttons(GTK_DIALOG(dialog), "_Cancel", GTK_RESPONSE_CANCEL, "_Delete", GTK_RESPONSE_ACCEPT, NULL);
|
||||
gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_CANCEL);
|
||||
confirmed = gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT;
|
||||
gtk_widget_destroy(dialog);
|
||||
g_free(message);
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
static char *fishlim_gui_selected_target(void) {
|
||||
GtkTreeSelection *selection;
|
||||
GtkTreeModel *model;
|
||||
GtkTreeIter iter;
|
||||
char *target = NULL;
|
||||
|
||||
if (!fishlim_view)
|
||||
return NULL;
|
||||
|
||||
selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(fishlim_view));
|
||||
if (gtk_tree_selection_get_selected(selection, &model, &iter))
|
||||
gtk_tree_model_get(model, &iter, 0, &target, -1);
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
static void fishlim_gui_delete(GtkWidget *widget, gpointer data) {
|
||||
char *selected = fishlim_gui_selected_target();
|
||||
const char *target = selected ? selected : fishlim_gui_target();
|
||||
|
||||
if (!target || !*target) {
|
||||
zoitechat_printf(ph, "%s\n", usage_delkey);
|
||||
g_free(selected);
|
||||
return;
|
||||
}
|
||||
|
||||
if (fishlim_gui_confirm_delete(target)) {
|
||||
zoitechat_commandf(ph, "DELKEY %s", target);
|
||||
fishlim_gui_refresh();
|
||||
}
|
||||
|
||||
g_free(selected);
|
||||
}
|
||||
|
||||
static char *fishlim_gui_prompt_target(const char *title, const char *initial) {
|
||||
GtkWidget *dialog, *content, *entry;
|
||||
char *target = NULL;
|
||||
|
||||
dialog = gtk_dialog_new_with_buttons(title, GTK_WINDOW(fishlim_dialog), GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, "_Cancel", GTK_RESPONSE_CANCEL, "_OK", GTK_RESPONSE_OK, NULL);
|
||||
content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
|
||||
entry = gtk_entry_new();
|
||||
if (initial && *initial)
|
||||
gtk_entry_set_text(GTK_ENTRY(entry), initial);
|
||||
gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);
|
||||
gtk_box_pack_start(GTK_BOX(content), gtk_label_new("Private message user"), FALSE, FALSE, 6);
|
||||
gtk_box_pack_start(GTK_BOX(content), entry, FALSE, FALSE, 6);
|
||||
gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_OK);
|
||||
gtk_widget_show_all(dialog);
|
||||
|
||||
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_OK) {
|
||||
const char *text = gtk_entry_get_text(GTK_ENTRY(entry));
|
||||
if (text && *text)
|
||||
target = g_strdup(text);
|
||||
}
|
||||
|
||||
gtk_widget_destroy(dialog);
|
||||
return target;
|
||||
}
|
||||
|
||||
static void fishlim_gui_keyx(GtkWidget *widget, gpointer data) {
|
||||
char *target;
|
||||
const char *initial = fishlim_gui_target();
|
||||
const char *mode = gtk_combo_box_get_active(GTK_COMBO_BOX(fishlim_mode_combo)) == 1 ? "CBC" : "ECB";
|
||||
|
||||
if (!initial || !*initial || !irc_is_query(initial))
|
||||
initial = NULL;
|
||||
|
||||
target = fishlim_gui_prompt_target("Key Exchange", initial);
|
||||
if (!target)
|
||||
return;
|
||||
|
||||
zoitechat_commandf(ph, "KEYX %s %s", target, mode);
|
||||
g_free(target);
|
||||
}
|
||||
|
||||
static void fishlim_gui_row_activated(GtkTreeView *tree, GtkTreePath *path, GtkTreeViewColumn *column, gpointer data) {
|
||||
GtkTreeModel *model = gtk_tree_view_get_model(tree);
|
||||
GtkTreeIter iter;
|
||||
char *target;
|
||||
|
||||
if (!gtk_tree_model_get_iter(model, &iter, path))
|
||||
return;
|
||||
|
||||
gtk_tree_model_get(model, &iter, 0, &target, -1);
|
||||
gtk_entry_set_text(GTK_ENTRY(fishlim_target_entry), target);
|
||||
g_free(target);
|
||||
fishlim_gui_refresh();
|
||||
}
|
||||
|
||||
static void fishlim_gui_destroy(GtkWidget *widget, gpointer data) {
|
||||
fishlim_dialog = NULL;
|
||||
fishlim_target_entry = NULL;
|
||||
fishlim_key_entry = NULL;
|
||||
fishlim_mode_combo = NULL;
|
||||
fishlim_status_label = NULL;
|
||||
fishlim_store = NULL;
|
||||
fishlim_view = NULL;
|
||||
}
|
||||
|
||||
static int handle_fishlim(char *word[], char *word_eol[], void *userdata) {
|
||||
GtkWidget *content, *grid, *view, *scroll, *buttons, *button;
|
||||
GtkCellRenderer *renderer;
|
||||
const char *target;
|
||||
|
||||
target = *word_eol[2] ? word_eol[2] : zoitechat_get_info(ph, "channel");
|
||||
|
||||
if (fishlim_dialog) {
|
||||
if (target)
|
||||
gtk_entry_set_text(GTK_ENTRY(fishlim_target_entry), target);
|
||||
gtk_window_present(GTK_WINDOW(fishlim_dialog));
|
||||
return ZOITECHAT_EAT_ZOITECHAT;
|
||||
}
|
||||
|
||||
fishlim_dialog = gtk_dialog_new_with_buttons("FiSHLiM Key Manager", NULL, GTK_DIALOG_DESTROY_WITH_PARENT, "_Close", GTK_RESPONSE_CLOSE, NULL);
|
||||
gtk_window_set_default_size(GTK_WINDOW(fishlim_dialog), 520, 360);
|
||||
g_signal_connect(fishlim_dialog, "response", G_CALLBACK(gtk_widget_destroy), NULL);
|
||||
g_signal_connect(fishlim_dialog, "destroy", G_CALLBACK(fishlim_gui_destroy), NULL);
|
||||
|
||||
content = gtk_dialog_get_content_area(GTK_DIALOG(fishlim_dialog));
|
||||
grid = gtk_grid_new();
|
||||
gtk_grid_set_row_spacing(GTK_GRID(grid), 8);
|
||||
gtk_grid_set_column_spacing(GTK_GRID(grid), 8);
|
||||
gtk_container_set_border_width(GTK_CONTAINER(grid), 12);
|
||||
gtk_box_pack_start(GTK_BOX(content), grid, TRUE, TRUE, 0);
|
||||
|
||||
fishlim_target_entry = gtk_entry_new();
|
||||
if (target)
|
||||
gtk_entry_set_text(GTK_ENTRY(fishlim_target_entry), target);
|
||||
gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Target nick or channel"), 0, 0, 1, 1);
|
||||
gtk_grid_attach(GTK_GRID(grid), fishlim_target_entry, 1, 0, 2, 1);
|
||||
|
||||
fishlim_mode_combo = gtk_combo_box_text_new();
|
||||
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(fishlim_mode_combo), "ECB");
|
||||
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(fishlim_mode_combo), "CBC");
|
||||
gtk_combo_box_set_active(GTK_COMBO_BOX(fishlim_mode_combo), 1);
|
||||
gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Mode"), 0, 1, 1, 1);
|
||||
gtk_grid_attach(GTK_GRID(grid), fishlim_mode_combo, 1, 1, 2, 1);
|
||||
gtk_grid_attach(GTK_GRID(grid), gtk_label_new("CBC may not work with older clients. Key exchange is private-message only."), 1, 2, 2, 1);
|
||||
|
||||
fishlim_key_entry = gtk_entry_new();
|
||||
gtk_entry_set_visibility(GTK_ENTRY(fishlim_key_entry), FALSE);
|
||||
gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Key"), 0, 3, 1, 1);
|
||||
gtk_grid_attach(GTK_GRID(grid), fishlim_key_entry, 1, 3, 2, 1);
|
||||
|
||||
buttons = gtk_button_box_new(GTK_ORIENTATION_HORIZONTAL);
|
||||
gtk_button_box_set_layout(GTK_BUTTON_BOX(buttons), GTK_BUTTONBOX_END);
|
||||
gtk_grid_attach(GTK_GRID(grid), buttons, 0, 4, 3, 1);
|
||||
|
||||
button = gtk_button_new_with_label("Set Key");
|
||||
g_signal_connect(button, "clicked", G_CALLBACK(fishlim_gui_set), NULL);
|
||||
gtk_container_add(GTK_CONTAINER(buttons), button);
|
||||
|
||||
button = gtk_button_new_with_label("Delete Key");
|
||||
g_signal_connect(button, "clicked", G_CALLBACK(fishlim_gui_delete), NULL);
|
||||
gtk_container_add(GTK_CONTAINER(buttons), button);
|
||||
|
||||
button = gtk_button_new_with_label("Private Message Key Exchange");
|
||||
g_signal_connect(button, "clicked", G_CALLBACK(fishlim_gui_keyx), NULL);
|
||||
gtk_container_add(GTK_CONTAINER(buttons), button);
|
||||
|
||||
fishlim_status_label = gtk_label_new(NULL);
|
||||
gtk_label_set_xalign(GTK_LABEL(fishlim_status_label), 0.0);
|
||||
gtk_grid_attach(GTK_GRID(grid), fishlim_status_label, 0, 5, 3, 1);
|
||||
|
||||
fishlim_store = gtk_list_store_new(3, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
|
||||
view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(fishlim_store));
|
||||
fishlim_view = view;
|
||||
renderer = gtk_cell_renderer_text_new();
|
||||
gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(view), -1, "Target", renderer, "text", 0, NULL);
|
||||
renderer = gtk_cell_renderer_text_new();
|
||||
gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(view), -1, "Mode", renderer, "text", 1, NULL);
|
||||
renderer = gtk_cell_renderer_text_new();
|
||||
gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(view), -1, "Status", renderer, "text", 2, NULL);
|
||||
g_signal_connect(view, "row-activated", G_CALLBACK(fishlim_gui_row_activated), NULL);
|
||||
|
||||
scroll = gtk_scrolled_window_new(NULL, NULL);
|
||||
gtk_widget_set_vexpand(scroll, TRUE);
|
||||
gtk_container_add(GTK_CONTAINER(scroll), view);
|
||||
gtk_grid_attach(GTK_GRID(grid), scroll, 0, 6, 3, 1);
|
||||
|
||||
g_signal_connect(fishlim_target_entry, "changed", G_CALLBACK(fishlim_gui_refresh_cb), NULL);
|
||||
fishlim_gui_refresh();
|
||||
|
||||
gtk_widget_show_all(fishlim_dialog);
|
||||
return ZOITECHAT_EAT_ZOITECHAT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Command handler for /setkey
|
||||
*/
|
||||
@@ -490,6 +806,8 @@ static int handle_setkey(char *word[], char *word_eol[], void *userdata) {
|
||||
/* Set password */
|
||||
if (keystore_store_key(nick, key, mode)) {
|
||||
zoitechat_printf(ph, "Stored key for %s (%s)\n", nick, fish_modes[mode]);
|
||||
if (mode == FISH_CBC_MODE)
|
||||
zoitechat_printf(ph, "Warning: CBC may not work with older clients.\n");
|
||||
} else {
|
||||
zoitechat_printf(ph, "\00305Failed to store key in addon_fishlim.conf\n");
|
||||
}
|
||||
@@ -533,8 +851,18 @@ static int handle_keyx(char *word[], char *word_eol[], void *userdata) {
|
||||
const char *target = word[2];
|
||||
zoitechat_context *query_ctx = NULL;
|
||||
char *pub_key, *priv_key;
|
||||
enum fish_mode mode = FISH_CBC_MODE;
|
||||
int ctx_type;
|
||||
|
||||
if (*word[3]) {
|
||||
if (g_ascii_strcasecmp(word[3], "ECB") == 0)
|
||||
mode = FISH_ECB_MODE;
|
||||
else if (g_ascii_strcasecmp(word[3], "CBC") != 0) {
|
||||
zoitechat_printf(ph, "%s", usage_keyx);
|
||||
return ZOITECHAT_EAT_ALL;
|
||||
}
|
||||
}
|
||||
|
||||
if (*target)
|
||||
query_ctx = find_context_on_network(target);
|
||||
else {
|
||||
@@ -555,8 +883,10 @@ static int handle_keyx(char *word[], char *word_eol[], void *userdata) {
|
||||
if (dh1080_generate_key(&priv_key, &pub_key)) {
|
||||
g_hash_table_replace (pending_exchanges, g_ascii_strdown(target, -1), priv_key);
|
||||
|
||||
zoitechat_commandf(ph, "quote NOTICE %s :DH1080_INIT %s CBC", target, pub_key);
|
||||
zoitechat_printf(ph, "Sent public key to %s (CBC), waiting for reply...", target);
|
||||
zoitechat_commandf(ph, "quote NOTICE %s :DH1080_INIT %s%s", target, pub_key, (mode == FISH_CBC_MODE) ? " CBC" : "");
|
||||
zoitechat_printf(ph, "Sent public key to %s (%s), waiting for reply...", target, fish_modes[mode]);
|
||||
if (mode == FISH_CBC_MODE)
|
||||
zoitechat_printf(ph, "Warning: CBC may not work with older clients.");
|
||||
|
||||
g_free(pub_key);
|
||||
} else {
|
||||
@@ -789,6 +1119,7 @@ int zoitechat_plugin_init(zoitechat_plugin *plugin_handle,
|
||||
*version = plugin_version;
|
||||
|
||||
/* Register commands */
|
||||
zoitechat_hook_command(ph, "FISHLIM", ZOITECHAT_PRI_NORM, handle_fishlim, "Usage: FISHLIM, opens the FiSHLiM key manager", NULL);
|
||||
zoitechat_hook_command(ph, "SETKEY", ZOITECHAT_PRI_NORM, handle_setkey, usage_setkey, NULL);
|
||||
zoitechat_hook_command(ph, "DELKEY", ZOITECHAT_PRI_NORM, handle_delkey, usage_delkey, NULL);
|
||||
zoitechat_hook_command(ph, "KEYX", ZOITECHAT_PRI_NORM, handle_keyx, usage_keyx, NULL);
|
||||
@@ -817,12 +1148,19 @@ int zoitechat_plugin_init(zoitechat_plugin *plugin_handle,
|
||||
|
||||
pending_exchanges = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
|
||||
|
||||
zoitechat_command(ph, "MENU ADD \"Window/FiSHLiM Key Manager\" \"FISHLIM\"");
|
||||
zoitechat_command(ph, "MENU ADD \"$NICK/FiSHLiM Key Manager\" \"FISHLIM %s\"");
|
||||
|
||||
zoitechat_printf(ph, "%s plugin loaded\n", plugin_name);
|
||||
/* Return success */
|
||||
return 1;
|
||||
}
|
||||
|
||||
int zoitechat_plugin_deinit(void) {
|
||||
zoitechat_command(ph, "MENU DEL \"Window/FiSHLiM Key Manager\"");
|
||||
zoitechat_command(ph, "MENU DEL \"$NICK/FiSHLiM Key Manager\"");
|
||||
if (fishlim_dialog)
|
||||
gtk_widget_destroy(fishlim_dialog);
|
||||
g_clear_pointer(&pending_exchanges, g_hash_table_destroy);
|
||||
dh1080_deinit();
|
||||
fish_deinit();
|
||||
|
||||
@@ -53,6 +53,16 @@ builder.set_source('_zoitechat_embedded', '''
|
||||
#include "config.h"
|
||||
#include "zoitechat-plugin.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
static zoitechat_plugin *ph;
|
||||
CFFI_DLLEXPORT int _on_plugin_init(char **, char **, char **, char *, char *);
|
||||
CFFI_DLLEXPORT int _on_plugin_deinit(void);
|
||||
@@ -61,6 +71,20 @@ int zoitechat_plugin_init(zoitechat_plugin *plugin_handle,
|
||||
char **name_out, char **description_out,
|
||||
char **version_out, char *arg)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
/* CPython cannot be removed from a process once Py_Initialize has run.
|
||||
If the embedded interpreter fails to start (e.g. _cffi_backend is
|
||||
missing), the host responds to our 0 return by g_module_close()ing
|
||||
this plugin, which would also drop the last reference to pythonXY.dll
|
||||
and unload it mid-flight; the leftover process-global state then
|
||||
crashes the process later, e.g. when a server connection spawns the
|
||||
next thread. Pin this module so FreeLibrary can never unload it. */
|
||||
HMODULE self_handle;
|
||||
GetModuleHandleExW (GET_MODULE_HANDLE_EX_FLAG_PIN |
|
||||
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
|
||||
(LPCWSTR)&zoitechat_plugin_init, &self_handle);
|
||||
#endif
|
||||
|
||||
if (ph != NULL)
|
||||
{
|
||||
puts ("Python plugin already loaded\\n");
|
||||
|
||||
@@ -19,7 +19,7 @@ else:
|
||||
if not hasattr(sys, 'argv'):
|
||||
sys.argv = ['<zoitechat>']
|
||||
|
||||
VERSION = b'2.18.1'
|
||||
VERSION = b'2.18.3'
|
||||
PLUGIN_NAME = ffi.new('char[]', b'Python')
|
||||
PLUGIN_DESC = ffi.new('char[]', b'Python %d.%d scripting interface' % (sys.version_info[0], sys.version_info[1]))
|
||||
PLUGIN_VERSION = ffi.new('char[]', VERSION)
|
||||
@@ -173,7 +173,7 @@ class Plugin:
|
||||
|
||||
def __del__(self):
|
||||
log('unloading', self.filename)
|
||||
for hook in self.hooks:
|
||||
for hook in list(self.hooks):
|
||||
if hook.is_unload is True:
|
||||
try:
|
||||
hook.callback(hook.userdata)
|
||||
|
||||
64
plugins/python/python38.vcxproj
Normal file
64
plugins/python/python38.vcxproj
Normal file
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Label="Configuration">
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Release|Win32">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{D58924D2-1492-4AC3-B058-7A214424EEDC}</ProjectGuid>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<RootNamespace>python38</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
<Import Project="..\..\win32\zoitechat.props" />
|
||||
<PropertyGroup>
|
||||
<TargetName>$(Python38Output)</TargetName>
|
||||
<OutDir>$(ZoiteChatRel)plugins\</OutDir>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>WIN32;_WIN64;_AMD64_;NDEBUG;_WINDOWS;_USRDLL;PYTHON_EXPORTS;Py_NO_LINK_LIB;$(OwnFlags);%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<UndefinePreprocessorDefinitions>_DEBUG;Py_DEBUG;Py_REF_DEBUG;Py_TRACE_REFS;%(UndefinePreprocessorDefinitions)</UndefinePreprocessorDefinitions>
|
||||
<AdditionalOptions>/U_DEBUG /UPy_DEBUG /UPy_REF_DEBUG /UPy_TRACE_REFS %(AdditionalOptions)</AdditionalOptions>
|
||||
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
||||
<AdditionalIncludeDirectories>$(Glib);$(Python38Path)\include;..\..\src\common;$(ZoiteChatLib);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<ModuleDefinitionFile>python.def</ModuleDefinitionFile>
|
||||
<AdditionalDependencies>"$(Python38Lib).lib";$(DepLibs);%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalLibraryDirectories>$(DepsRoot)\lib;$(Python38Path)\libs;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
</Link>
|
||||
<PreBuildEvent>
|
||||
<Command>"$(Python38Path)\python.exe" generate_plugin.py ..\..\src\common\zoitechat-plugin.h python.py "$(IntDir)python.c"</Command>
|
||||
</PreBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<None Include="generate_plugin.py" />
|
||||
<None Include="zoitechat.py" />
|
||||
<None Include="python.def" />
|
||||
<None Include="python.py" />
|
||||
<None Include="xchat.py" />
|
||||
<None Include="hexchat.py" />
|
||||
<None Include="_zoitechat.py" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="$(IntDir)python.c" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
</Project>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -428,6 +428,7 @@ const struct prefs vars[] =
|
||||
{"gui_lagometer", P_OFFINT (hex_gui_lagometer), TYPE_INT},
|
||||
{"gui_lang", P_OFFINT (hex_gui_lang), TYPE_INT},
|
||||
{"gui_mode_buttons", P_OFFINT (hex_gui_mode_buttons), TYPE_BOOL},
|
||||
{"gui_mode_buttons_inline", P_OFFINT (hex_gui_mode_buttons_inline), TYPE_BOOL},
|
||||
{"gui_pane_divider_position", P_OFFINT (hex_gui_pane_divider_position), TYPE_INT},
|
||||
{"gui_pane_left_size", P_OFFINT (hex_gui_pane_left_size), TYPE_INT},
|
||||
{"gui_pane_right_size", P_OFFINT (hex_gui_pane_right_size), TYPE_INT},
|
||||
@@ -458,6 +459,7 @@ const struct prefs vars[] =
|
||||
{"gui_tab_utils", P_OFFINT (hex_gui_tab_utils), TYPE_BOOL},
|
||||
{"gui_throttlemeter", P_OFFINT (hex_gui_throttlemeter), TYPE_INT},
|
||||
{"gui_topicbar", P_OFFINT (hex_gui_topicbar), TYPE_BOOL},
|
||||
{"gui_topicbar_multiline", P_OFFINT (hex_gui_topicbar_multiline), TYPE_BOOL},
|
||||
{"gui_transparency", P_OFFINT (hex_gui_transparency), TYPE_INT},
|
||||
{"gui_tray", P_OFFINT (hex_gui_tray), TYPE_BOOL},
|
||||
{"gui_tray_away", P_OFFINT (hex_gui_tray_away), TYPE_BOOL},
|
||||
@@ -518,6 +520,7 @@ const struct prefs vars[] =
|
||||
{"irc_conf_mode", P_OFFINT (hex_irc_conf_mode), TYPE_BOOL},
|
||||
{"irc_extra_hilight", P_OFFSET (hex_irc_extra_hilight), TYPE_STR},
|
||||
{"irc_hide_nickchange", P_OFFINT (hex_irc_hide_nickchange), TYPE_BOOL},
|
||||
{"irc_hide_join_part_hostmask", P_OFFINT (hex_irc_hide_join_part_hostmask), TYPE_BOOL},
|
||||
{"irc_hide_version", P_OFFINT (hex_irc_hide_version), TYPE_BOOL},
|
||||
{"irc_hidehost", P_OFFINT (hex_irc_hidehost), TYPE_BOOL},
|
||||
{"irc_id_ntext", P_OFFSET (hex_irc_id_ntext), TYPE_STR},
|
||||
@@ -790,6 +793,7 @@ load_default_config(void)
|
||||
prefs.hex_gui_tab_scrollchans = 1;
|
||||
prefs.hex_gui_mouse_scroll_speed = 10;
|
||||
prefs.hex_gui_topicbar = 1;
|
||||
prefs.hex_gui_topicbar_multiline = 1;
|
||||
prefs.hex_gui_transparency = 255;
|
||||
prefs.hex_gui_tray = 1;
|
||||
prefs.hex_gui_tray_blink = 1;
|
||||
|
||||
@@ -1090,13 +1090,19 @@ clients_find_filename_foreach (gpointer key,
|
||||
gpointer user_data)
|
||||
{
|
||||
RemoteObject *obj = value;
|
||||
return g_str_equal (obj->filename, (char *)user_data);
|
||||
|
||||
return obj->filename != NULL && g_str_equal (obj->filename, user_data);
|
||||
}
|
||||
|
||||
static int
|
||||
unload_plugin_cb (char *word[], char *word_eol[], void *userdata)
|
||||
{
|
||||
RemoteObject *obj = g_hash_table_find (clients, clients_find_filename_foreach, word[2]);
|
||||
RemoteObject *obj;
|
||||
|
||||
if (word[2][0] == 0)
|
||||
return ZOITECHAT_EAT_NONE;
|
||||
|
||||
obj = g_hash_table_find (clients, clients_find_filename_foreach, word[2]);
|
||||
|
||||
if (obj != NULL)
|
||||
{
|
||||
|
||||
@@ -78,6 +78,7 @@ int fe_input_add (int sok, int flags, void *func, void *data);
|
||||
void fe_input_remove (int tag);
|
||||
void fe_idle_add (void *func, void *data);
|
||||
void fe_set_topic (struct session *sess, char *topic, char *stripped_topic);
|
||||
void fe_set_typing (struct session *sess, const char *nick, const char *state);
|
||||
typedef enum
|
||||
{
|
||||
FE_COLOR_NONE = 0,
|
||||
@@ -141,6 +142,7 @@ void fe_set_away (server *serv);
|
||||
void fe_serverlist_open (session *sess);
|
||||
void fe_get_bool (char *title, char *prompt, void *callback, void *userdata);
|
||||
void fe_get_str (char *prompt, char *def, void *callback, void *ud);
|
||||
void fe_get_password (char *prompt, void *callback, void *ud);
|
||||
void fe_get_int (char *prompt, int def, void *callback, void *ud);
|
||||
#define FRF_WRITE 1 /* save file */
|
||||
#define FRF_MULTIPLE 2 /* multi-select */
|
||||
|
||||
@@ -315,6 +315,59 @@ resolve_parent_theme_root (const char *child_theme_root, const char *parent_name
|
||||
return home_local;
|
||||
g_free (home_local);
|
||||
|
||||
{
|
||||
const char *xdg_data_home = g_getenv ("XDG_DATA_HOME");
|
||||
|
||||
if (xdg_data_home && xdg_data_home[0])
|
||||
{
|
||||
candidate = g_build_filename (xdg_data_home, "themes", parent_name, NULL);
|
||||
if (path_exists_as_dir (candidate))
|
||||
return candidate;
|
||||
g_free (candidate);
|
||||
}
|
||||
}
|
||||
|
||||
candidate = g_build_filename (g_get_user_data_dir (), "themes", parent_name, NULL);
|
||||
if (path_exists_as_dir (candidate))
|
||||
return candidate;
|
||||
g_free (candidate);
|
||||
|
||||
{
|
||||
const char *xdg_data_dirs = g_getenv ("XDG_DATA_DIRS");
|
||||
guint i;
|
||||
|
||||
if (xdg_data_dirs && xdg_data_dirs[0])
|
||||
{
|
||||
char **data_dirs = g_strsplit (xdg_data_dirs, G_SEARCHPATH_SEPARATOR_S, -1);
|
||||
|
||||
for (i = 0; data_dirs && data_dirs[i]; i++)
|
||||
{
|
||||
if (!data_dirs[i][0])
|
||||
continue;
|
||||
candidate = g_build_filename (data_dirs[i], "themes", parent_name, NULL);
|
||||
if (path_exists_as_dir (candidate))
|
||||
{
|
||||
g_strfreev (data_dirs);
|
||||
return candidate;
|
||||
}
|
||||
g_free (candidate);
|
||||
}
|
||||
g_strfreev (data_dirs);
|
||||
}
|
||||
else
|
||||
{
|
||||
const char *const *system_data_dirs = g_get_system_data_dirs ();
|
||||
|
||||
for (i = 0; system_data_dirs && system_data_dirs[i]; i++)
|
||||
{
|
||||
candidate = g_build_filename (system_data_dirs[i], "themes", parent_name, NULL);
|
||||
if (path_exists_as_dir (candidate))
|
||||
return candidate;
|
||||
g_free (candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
user_dir = zoitechat_gtk3_theme_service_get_user_themes_dir ();
|
||||
candidate = g_build_filename (user_dir, parent_name, NULL);
|
||||
g_free (user_dir);
|
||||
@@ -632,18 +685,55 @@ zoitechat_gtk3_theme_service_discover (void)
|
||||
|
||||
g_mkdir_with_parents (user_dir, 0700);
|
||||
|
||||
user_data_themes = g_build_filename (g_get_user_data_dir (), "themes", NULL);
|
||||
/* Prefer the live XDG environment over GLib's process-cached copies so
|
||||
* theme lookup matches what GTK resolves for the running session. */
|
||||
{
|
||||
const char *xdg_data_home = g_getenv ("XDG_DATA_HOME");
|
||||
|
||||
if (xdg_data_home && xdg_data_home[0])
|
||||
user_data_themes = g_build_filename (xdg_data_home, "themes", NULL);
|
||||
else
|
||||
user_data_themes = g_build_filename (g_get_user_data_dir (), "themes", NULL);
|
||||
}
|
||||
/* Scan the app's own themes dir first: imported themes get a symlink
|
||||
* under the XDG data themes dir so GTK can resolve gtk-theme-name, and
|
||||
* scanning that link first would make the canonical-root dedup keep
|
||||
* the symlink path - changing the theme's path-derived id and breaking
|
||||
* the saved gui_gtk3_theme reference on the next start. */
|
||||
add_theme_root (user_roots, seen_user_roots, user_dir);
|
||||
add_theme_root (user_roots, seen_user_roots, user_data_themes);
|
||||
g_free (user_data_themes);
|
||||
add_theme_root (user_roots, seen_user_roots, home_themes);
|
||||
add_theme_root (user_roots, seen_user_roots, user_dir);
|
||||
|
||||
system_data_dirs = g_get_system_data_dirs ();
|
||||
for (i = 0; system_data_dirs && system_data_dirs[i]; i++)
|
||||
{
|
||||
char *system_themes = g_build_filename (system_data_dirs[i], "themes", NULL);
|
||||
add_theme_root (system_roots, seen_system_roots, system_themes);
|
||||
g_free (system_themes);
|
||||
const char *xdg_data_dirs = g_getenv ("XDG_DATA_DIRS");
|
||||
|
||||
if (xdg_data_dirs && xdg_data_dirs[0])
|
||||
{
|
||||
char **data_dirs = g_strsplit (xdg_data_dirs, G_SEARCHPATH_SEPARATOR_S, -1);
|
||||
|
||||
for (i = 0; data_dirs && data_dirs[i]; i++)
|
||||
{
|
||||
char *system_themes;
|
||||
|
||||
if (!data_dirs[i][0])
|
||||
continue;
|
||||
system_themes = g_build_filename (data_dirs[i], "themes", NULL);
|
||||
add_theme_root (system_roots, seen_system_roots, system_themes);
|
||||
g_free (system_themes);
|
||||
}
|
||||
g_strfreev (data_dirs);
|
||||
}
|
||||
else
|
||||
{
|
||||
system_data_dirs = g_get_system_data_dirs ();
|
||||
for (i = 0; system_data_dirs && system_data_dirs[i]; i++)
|
||||
{
|
||||
char *system_themes = g_build_filename (system_data_dirs[i], "themes", NULL);
|
||||
add_theme_root (system_roots, seen_system_roots, system_themes);
|
||||
g_free (system_themes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < system_roots->len; i++)
|
||||
@@ -922,7 +1012,7 @@ static gboolean
|
||||
validate_theme_root_for_import (const char *theme_root, GError **error)
|
||||
{
|
||||
char *index_theme;
|
||||
GKeyFile *keyfile;
|
||||
GKeyFile *keyfile = NULL;
|
||||
char *css_dir;
|
||||
char *css_path;
|
||||
char *raw_inherits;
|
||||
@@ -930,36 +1020,33 @@ validate_theme_root_for_import (const char *theme_root, GError **error)
|
||||
guint i;
|
||||
GError *load_error = NULL;
|
||||
|
||||
/* index.theme is optional for GTK3 CSS themes (GTK only needs
|
||||
* gtk-3.x/gtk.css), but when it is present it must be well formed so
|
||||
* broken archives are rejected up front. */
|
||||
index_theme = g_build_filename (theme_root, "index.theme", NULL);
|
||||
if (!g_file_test (index_theme, G_FILE_TEST_IS_REGULAR))
|
||||
if (g_file_test (index_theme, G_FILE_TEST_IS_REGULAR))
|
||||
{
|
||||
g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
|
||||
"Invalid GTK3 theme at '%s': missing required index.theme at '%s'.",
|
||||
theme_root, index_theme);
|
||||
g_free (index_theme);
|
||||
return FALSE;
|
||||
}
|
||||
keyfile = g_key_file_new ();
|
||||
if (!g_key_file_load_from_file (keyfile, index_theme, G_KEY_FILE_NONE, &load_error))
|
||||
{
|
||||
g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
|
||||
"Invalid GTK3 theme at '%s': failed to parse index.theme '%s': %s.",
|
||||
theme_root, index_theme, load_error->message);
|
||||
g_error_free (load_error);
|
||||
g_key_file_unref (keyfile);
|
||||
g_free (index_theme);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
keyfile = g_key_file_new ();
|
||||
if (!g_key_file_load_from_file (keyfile, index_theme, G_KEY_FILE_NONE, &load_error))
|
||||
{
|
||||
g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
|
||||
"Invalid GTK3 theme at '%s': failed to parse index.theme '%s': %s.",
|
||||
theme_root, index_theme, load_error->message);
|
||||
g_error_free (load_error);
|
||||
g_key_file_unref (keyfile);
|
||||
g_free (index_theme);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (!g_key_file_has_group (keyfile, "Desktop Entry"))
|
||||
{
|
||||
g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
|
||||
"Invalid GTK3 theme at '%s': index.theme '%s' is missing the [Desktop Entry] section.",
|
||||
theme_root, index_theme);
|
||||
g_key_file_unref (keyfile);
|
||||
g_free (index_theme);
|
||||
return FALSE;
|
||||
if (!g_key_file_has_group (keyfile, "Desktop Entry"))
|
||||
{
|
||||
g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
|
||||
"Invalid GTK3 theme at '%s': index.theme '%s' is missing the [Desktop Entry] section.",
|
||||
theme_root, index_theme);
|
||||
g_key_file_unref (keyfile);
|
||||
g_free (index_theme);
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
css_dir = zoitechat_gtk3_theme_pick_css_dir (theme_root);
|
||||
@@ -968,7 +1055,8 @@ validate_theme_root_for_import (const char *theme_root, GError **error)
|
||||
g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
|
||||
"Invalid GTK3 theme at '%s': could not resolve a GTK CSS directory (expected gtk-3.x/gtk.css).",
|
||||
theme_root);
|
||||
g_key_file_unref (keyfile);
|
||||
if (keyfile)
|
||||
g_key_file_unref (keyfile);
|
||||
g_free (index_theme);
|
||||
return FALSE;
|
||||
}
|
||||
@@ -981,15 +1069,20 @@ validate_theme_root_for_import (const char *theme_root, GError **error)
|
||||
theme_root, css_path);
|
||||
g_free (css_path);
|
||||
g_free (css_dir);
|
||||
g_key_file_unref (keyfile);
|
||||
if (keyfile)
|
||||
g_key_file_unref (keyfile);
|
||||
g_free (index_theme);
|
||||
return FALSE;
|
||||
}
|
||||
g_free (css_path);
|
||||
g_free (css_dir);
|
||||
|
||||
raw_inherits = g_key_file_get_string (keyfile, "Desktop Entry", "Inherits", NULL);
|
||||
g_key_file_unref (keyfile);
|
||||
raw_inherits = NULL;
|
||||
if (keyfile)
|
||||
{
|
||||
raw_inherits = g_key_file_get_string (keyfile, "Desktop Entry", "Inherits", NULL);
|
||||
g_key_file_unref (keyfile);
|
||||
}
|
||||
g_free (index_theme);
|
||||
if (!raw_inherits)
|
||||
return TRUE;
|
||||
|
||||
@@ -46,6 +46,155 @@
|
||||
#include "servlist.h"
|
||||
#include "sts.h"
|
||||
#include "text.h"
|
||||
|
||||
void reply_state_clear (session *sess);
|
||||
|
||||
gboolean
|
||||
reply_msgid_valid (const char *msgid)
|
||||
{
|
||||
const char *p;
|
||||
|
||||
if (!msgid || !*msgid || *msgid == ':')
|
||||
return FALSE;
|
||||
|
||||
for (p = msgid; *p; p++)
|
||||
{
|
||||
if (*p == ' ' || *p == '\r' || *p == '\n')
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
reply_item_free (reply_item *item)
|
||||
{
|
||||
if (!item)
|
||||
return;
|
||||
g_free (item->msgid);
|
||||
g_free (item->nick);
|
||||
g_free (item->text);
|
||||
g_free (item);
|
||||
}
|
||||
|
||||
void
|
||||
reply_cache_free (session *sess)
|
||||
{
|
||||
if (!sess)
|
||||
return;
|
||||
g_slist_free_full (sess->reply_items, (GDestroyNotify) reply_item_free);
|
||||
sess->reply_items = NULL;
|
||||
reply_state_clear (sess);
|
||||
}
|
||||
|
||||
reply_item *
|
||||
reply_cache_find (session *sess, const char *msgid)
|
||||
{
|
||||
GSList *list;
|
||||
|
||||
if (!sess || !reply_msgid_valid (msgid))
|
||||
return NULL;
|
||||
|
||||
for (list = sess->reply_items; list; list = list->next)
|
||||
{
|
||||
reply_item *item = list->data;
|
||||
if (!strcmp (item->msgid, msgid))
|
||||
return item;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
reply_item *
|
||||
reply_cache_latest_from (session *sess, const char *nick)
|
||||
{
|
||||
GSList *list;
|
||||
|
||||
if (!sess || !nick || !*nick)
|
||||
return NULL;
|
||||
|
||||
for (list = sess->reply_items; list; list = list->next)
|
||||
{
|
||||
reply_item *item = list->data;
|
||||
if (!sess->server->p_cmp (item->nick, nick))
|
||||
return item;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void
|
||||
reply_state_clear (session *sess)
|
||||
{
|
||||
if (!sess)
|
||||
return;
|
||||
g_clear_pointer (&sess->reply_msgid, g_free);
|
||||
g_clear_pointer (&sess->reply_target, g_free);
|
||||
g_clear_pointer (&sess->reply_nick, g_free);
|
||||
g_clear_pointer (&sess->reply_text, g_free);
|
||||
}
|
||||
|
||||
void
|
||||
reply_state_set (session *sess, const char *msgid, const char *target, const char *nick, const char *text)
|
||||
{
|
||||
if (!sess || !reply_msgid_valid (msgid))
|
||||
return;
|
||||
reply_state_clear (sess);
|
||||
sess->reply_msgid = g_strdup (msgid);
|
||||
sess->reply_target = g_strdup (target && *target ? target : sess->channel);
|
||||
sess->reply_nick = g_strdup (nick && *nick ? nick : _("message"));
|
||||
sess->reply_text = g_strdup (text && *text ? text : _("Original message unavailable"));
|
||||
}
|
||||
|
||||
static void
|
||||
reply_cache_add (session *sess, const char *msgid, const char *nick, const char *text, time_t timestamp)
|
||||
{
|
||||
reply_item *item;
|
||||
|
||||
if (!sess || !reply_msgid_valid (msgid) || !nick || !text)
|
||||
return;
|
||||
|
||||
item = reply_cache_find (sess, msgid);
|
||||
if (item)
|
||||
{
|
||||
g_free (item->nick);
|
||||
g_free (item->text);
|
||||
item->nick = g_strdup (nick);
|
||||
item->text = g_strdup (text);
|
||||
item->timestamp = timestamp;
|
||||
return;
|
||||
}
|
||||
|
||||
item = g_new0 (reply_item, 1);
|
||||
item->msgid = g_strdup (msgid);
|
||||
item->nick = g_strdup (nick);
|
||||
item->text = g_strdup (text);
|
||||
item->timestamp = timestamp;
|
||||
sess->reply_items = g_slist_prepend (sess->reply_items, item);
|
||||
|
||||
if (g_slist_length (sess->reply_items) > 200)
|
||||
{
|
||||
GSList *last = g_slist_last (sess->reply_items);
|
||||
item = last->data;
|
||||
sess->reply_items = g_slist_delete_link (sess->reply_items, last);
|
||||
reply_item_free (item);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
reply_context_print (session *sess, const message_tags_data *tags_data)
|
||||
{
|
||||
reply_item *item;
|
||||
|
||||
if (!sess || !tags_data || !tags_data->reply)
|
||||
return;
|
||||
|
||||
item = reply_cache_find (sess, tags_data->reply);
|
||||
if (item)
|
||||
PrintTextTimeStampf (sess, tags_data->timestamp, "\00314│ ↪ %s: %.180s\017\n", item->nick, item->text);
|
||||
else
|
||||
PrintTextTimeStamp (sess, "\00314│ ↪ Original message unavailable\017\n", tags_data->timestamp);
|
||||
}
|
||||
#include "ctcp.h"
|
||||
#include "zoitechatc.h"
|
||||
#include "chanopt.h"
|
||||
@@ -450,6 +599,9 @@ inbound_action (session *sess, char *chan, char *from, char *ip, char *text,
|
||||
fromme = TRUE;
|
||||
}
|
||||
|
||||
if (!fromme)
|
||||
fe_set_typing (sess, from, "done");
|
||||
|
||||
inbound_make_idtext (serv, idtext, sizeof (idtext), id);
|
||||
|
||||
if (!fromme && !privaction)
|
||||
@@ -518,12 +670,17 @@ inbound_chanmsg (server *serv, session *sess, char *chan, char *from,
|
||||
fromme = TRUE;
|
||||
}
|
||||
|
||||
if (!fromme)
|
||||
fe_set_typing (sess, from, "done");
|
||||
|
||||
if (fromme)
|
||||
{
|
||||
if (prefs.hex_away_auto_unmark && serv->is_away && !tags_data->timestamp)
|
||||
sess->server->p_set_back (sess->server);
|
||||
reply_context_print (sess, tags_data);
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_UCHANMSG, sess, from, text, nickchar, NULL,
|
||||
0, tags_data->timestamp);
|
||||
reply_cache_add (sess, tags_data->msgid, from, text, tags_data->timestamp);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -532,6 +689,8 @@ inbound_chanmsg (server *serv, session *sess, char *chan, char *from,
|
||||
if (is_hilight (from, text, sess, serv))
|
||||
hilight = TRUE;
|
||||
|
||||
reply_context_print (sess, tags_data);
|
||||
|
||||
if (sess->type == SESS_DIALOG)
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_DPRIVMSG, sess, from, text, idtext, NULL, 0,
|
||||
tags_data->timestamp);
|
||||
@@ -541,6 +700,8 @@ inbound_chanmsg (server *serv, session *sess, char *chan, char *from,
|
||||
else
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_CHANMSG, sess, from, text, nickchar, idtext,
|
||||
0, tags_data->timestamp);
|
||||
|
||||
reply_cache_add (sess, tags_data->msgid, from, text, tags_data->timestamp);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1483,7 +1644,7 @@ inbound_foundip (session *sess, char *ip, const message_tags_data *tags_data)
|
||||
{
|
||||
sess->server->dcc_ip = addr.s_addr;
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_FOUNDIP, sess->server->server_session,
|
||||
inet_ntoa (addr),
|
||||
net_ip (ntohl (addr.s_addr)),
|
||||
NULL, NULL, NULL, 0, tags_data->timestamp);
|
||||
}
|
||||
}
|
||||
@@ -1654,14 +1815,14 @@ inbound_login_end (session *sess, char *text, const message_tags_data *tags_data
|
||||
return;
|
||||
|
||||
/* send nickserv password */
|
||||
if (net && net->pass && inbound_nickserv_login (serv))
|
||||
if (net && serv->password[0] && inbound_nickserv_login (serv))
|
||||
{
|
||||
serv->p_ns_identify (serv, net->pass);
|
||||
serv->p_ns_identify (serv, serv->password);
|
||||
}
|
||||
|
||||
/* wait for join if command or nickserv set */
|
||||
if (net && prefs.hex_irc_join_delay
|
||||
&& ((net->pass && inbound_nickserv_login (serv))
|
||||
&& ((serv->password[0] && inbound_nickserv_login (serv))
|
||||
|| net->commandlist))
|
||||
{
|
||||
serv->joindelay_tag = fe_timeout_add_seconds (prefs.hex_irc_join_delay, check_autojoin_channels, serv);
|
||||
@@ -1742,6 +1903,10 @@ inbound_toggle_caps (server *serv, const char *extensions_str, gboolean enable)
|
||||
serv->have_awaynotify = enable;
|
||||
else if (!strcmp (extension, "account-tag"))
|
||||
serv->have_account_tag = enable;
|
||||
else if (!strcmp (extension, "message-tags"))
|
||||
serv->have_message_tags = enable;
|
||||
else if (!strcmp (extension, "echo-message"))
|
||||
serv->have_echo_message = enable;
|
||||
else if (!strcmp (extension, "sasl"))
|
||||
{
|
||||
serv->have_sasl = enable;
|
||||
@@ -1873,6 +2038,8 @@ static const char * const supported_caps[] = {
|
||||
"account-tag",
|
||||
"extended-monitor",
|
||||
"standard-replies",
|
||||
"message-tags",
|
||||
"echo-message",
|
||||
|
||||
/* ZNC */
|
||||
"znc.in/server-time-iso",
|
||||
|
||||
@@ -78,6 +78,13 @@ void inbound_login_end (session *sess, char *text,
|
||||
void inbound_chanmsg (server *serv, session *sess, char *chan, char *from,
|
||||
char *text, char fromme, int id,
|
||||
const message_tags_data *tags_data);
|
||||
gboolean reply_msgid_valid (const char *msgid);
|
||||
void reply_cache_free (session *sess);
|
||||
reply_item *reply_cache_find (session *sess, const char *msgid);
|
||||
reply_item *reply_cache_latest_from (session *sess, const char *nick);
|
||||
void reply_state_set (session *sess, const char *msgid, const char *target,
|
||||
const char *nick, const char *text);
|
||||
void reply_state_clear (session *sess);
|
||||
void clear_channel (session *sess);
|
||||
void set_topic (session *sess, char *topic, char *stripped_topic);
|
||||
void inbound_privmsg (server *serv, char *from, char *ip, char *text, int id,
|
||||
|
||||
@@ -907,6 +907,10 @@ inbound_005 (server * serv, char *word[], const message_tags_data *tags_data)
|
||||
{
|
||||
if (g_strcmp0 (tokvalue, "ascii") == 0)
|
||||
serv->p_cmp = (void *)g_ascii_strcasecmp;
|
||||
} else if (g_strcmp0 (tokname, "CLIENTTAGDENY") == 0)
|
||||
{
|
||||
g_free (serv->clienttagdeny);
|
||||
serv->clienttagdeny = tokadding ? g_strdup (tokvalue) : NULL;
|
||||
} else if (g_strcmp0 (tokname, "CHARSET") == 0)
|
||||
{
|
||||
if (g_ascii_strcasecmp (tokvalue, "UTF-8") == 0)
|
||||
|
||||
@@ -90,10 +90,26 @@ net_set_socket_options (int sok)
|
||||
char *
|
||||
net_ip (uint32_t addr)
|
||||
{
|
||||
static char buf[INET_ADDRSTRLEN];
|
||||
struct in_addr ia;
|
||||
|
||||
ia.s_addr = htonl (addr);
|
||||
return inet_ntoa (ia);
|
||||
if (!inet_ntop (AF_INET, &ia, buf, sizeof (buf)))
|
||||
buf[0] = 0;
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
int
|
||||
net_parse_ipv4 (const char *hostname, uint32_t *addr)
|
||||
{
|
||||
struct in_addr ia;
|
||||
|
||||
if (inet_pton (AF_INET, hostname, &ia) != 1)
|
||||
return FALSE;
|
||||
|
||||
*addr = ia.s_addr;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
int
|
||||
|
||||
@@ -39,6 +39,7 @@ int net_connect (netstore *ns, int sok4, int sok6, int *sok_return);
|
||||
char *net_resolve (netstore *ns, char *hostname, int port, char **real_host);
|
||||
void net_bind (netstore *tobindto, int sok4, int sok6);
|
||||
char *net_ip (uint32_t addr);
|
||||
int net_parse_ipv4 (const char *hostname, uint32_t *addr);
|
||||
int net_lookup_ipv4 (const char *hostname, uint32_t *addr);
|
||||
void net_sockets (int *sok4, int *sok6);
|
||||
|
||||
|
||||
@@ -468,7 +468,7 @@ create_mask (session * sess, char *mask, char *mode, char *typestr, int deop)
|
||||
type = prefs.hex_irc_ban_type;
|
||||
|
||||
buf[0] = 0;
|
||||
if (inet_addr (fullhost) != (guint32) -1) /* "fullhost" is really a IP number */
|
||||
if (net_parse_ipv4 (fullhost, &(guint32){0})) /* "fullhost" is really a IP number */
|
||||
{
|
||||
lastdot = strrchr (fullhost, '.');
|
||||
if (!lastdot)
|
||||
@@ -2744,8 +2744,8 @@ cmd_me (struct session *sess, char *tbuf, char *word[], char *word_eol[])
|
||||
while ((split_text = split_up_text (sess, act + offset, cmd_length, split_text)))
|
||||
{
|
||||
sess->server->p_action (sess->server, sess->channel, split_text);
|
||||
/* print it to screen */
|
||||
inbound_action (sess, sess->channel, sess->server->nick, "",
|
||||
if (!sess->server->have_echo_message)
|
||||
inbound_action (sess, sess->channel, sess->server->nick, "",
|
||||
split_text, TRUE, FALSE,
|
||||
&no_tags);
|
||||
|
||||
@@ -2756,8 +2756,8 @@ cmd_me (struct session *sess, char *tbuf, char *word[], char *word_eol[])
|
||||
}
|
||||
|
||||
sess->server->p_action (sess->server, sess->channel, act + offset);
|
||||
/* print it to screen */
|
||||
inbound_action (sess, sess->channel, sess->server->nick, "",
|
||||
if (!sess->server->have_echo_message)
|
||||
inbound_action (sess, sess->channel, sess->server->nick, "",
|
||||
act + offset, TRUE, FALSE, &no_tags);
|
||||
} else
|
||||
{
|
||||
@@ -2821,6 +2821,137 @@ cmd_mop (struct session *sess, char *tbuf, char *word[], char *word_eol[])
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
|
||||
static gboolean
|
||||
client_tag_allowed (server *serv, const char *tag)
|
||||
{
|
||||
char **deny;
|
||||
int i;
|
||||
|
||||
if (!serv->have_message_tags)
|
||||
return FALSE;
|
||||
|
||||
if (!serv->clienttagdeny || !*serv->clienttagdeny)
|
||||
return TRUE;
|
||||
|
||||
deny = g_strsplit (serv->clienttagdeny, ",", 0);
|
||||
for (i = 0; deny[i]; i++)
|
||||
{
|
||||
if (!strcmp (deny[i], "*") || !strcmp (deny[i], tag) || (deny[i][0] == '+' && !strcmp (deny[i] + 1, tag)))
|
||||
{
|
||||
g_strfreev (deny);
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
g_strfreev (deny);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static char *
|
||||
client_tag_escape (const char *text)
|
||||
{
|
||||
GString *out;
|
||||
const char *p;
|
||||
|
||||
out = g_string_sized_new (strlen (text));
|
||||
for (p = text; *p; p++)
|
||||
{
|
||||
switch (*p)
|
||||
{
|
||||
case ';':
|
||||
g_string_append (out, "\\:");
|
||||
break;
|
||||
case ' ':
|
||||
g_string_append (out, "\\s");
|
||||
break;
|
||||
case '\\':
|
||||
g_string_append (out, "\\\\");
|
||||
break;
|
||||
case '\r':
|
||||
g_string_append (out, "\\r");
|
||||
break;
|
||||
case '\n':
|
||||
g_string_append (out, "\\n");
|
||||
break;
|
||||
default:
|
||||
g_string_append_c (out, *p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return g_string_free (out, FALSE);
|
||||
}
|
||||
|
||||
static int
|
||||
cmd_reply (struct session *sess, char *tbuf, char *word[], char *word_eol[])
|
||||
{
|
||||
char *msgid = word[2];
|
||||
char *target = sess->channel;
|
||||
char *text = word_eol[3];
|
||||
char *escaped;
|
||||
char *tags;
|
||||
|
||||
if (!reply_msgid_valid (msgid) || !*target || !*text)
|
||||
return FALSE;
|
||||
|
||||
if (*text == ':')
|
||||
text++;
|
||||
|
||||
if (!sess->server->connected || !client_tag_allowed (sess->server, "reply"))
|
||||
{
|
||||
notc_msg (sess);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
escaped = client_tag_escape (msgid);
|
||||
tags = g_strdup_printf ("+reply=%s", escaped);
|
||||
sess->server->p_message_tagged (sess->server, tags, target, text);
|
||||
if (!sess->server->have_echo_message)
|
||||
{
|
||||
session *target_sess = find_dialog (sess->server, target);
|
||||
message_tags_data no_tags = MESSAGE_TAGS_DATA_INIT;
|
||||
no_tags.reply = msgid;
|
||||
|
||||
if (!target_sess)
|
||||
target_sess = find_channel (sess->server, target);
|
||||
if (target_sess)
|
||||
inbound_chanmsg (target_sess->server, target_sess, target_sess->channel, target_sess->server->nick, text, TRUE, FALSE, &no_tags);
|
||||
}
|
||||
g_free (tags);
|
||||
g_free (escaped);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static int
|
||||
cmd_typing (struct session *sess, char *tbuf, char *word[], char *word_eol[])
|
||||
{
|
||||
char *state = word[2];
|
||||
char *target = word[3];
|
||||
char tags[32];
|
||||
|
||||
if (!*state)
|
||||
state = "active";
|
||||
|
||||
if (!*target)
|
||||
target = sess->channel;
|
||||
|
||||
if (!*target || (strcmp (state, "active") && strcmp (state, "paused") && strcmp (state, "done")))
|
||||
return FALSE;
|
||||
|
||||
if (!sess->server->connected || !client_tag_allowed (sess->server, "typing"))
|
||||
{
|
||||
notc_msg (sess);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
g_snprintf (tags, sizeof (tags), "+typing=%s", state);
|
||||
sess->server->p_tagmsg (sess->server, tags, target);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static int
|
||||
cmd_msg (struct session *sess, char *tbuf, char *word[], char *word_eol[])
|
||||
{
|
||||
@@ -2875,7 +3006,7 @@ cmd_msg (struct session *sess, char *tbuf, char *word[], char *word_eol[])
|
||||
newsess = find_dialog (sess->server, nick);
|
||||
if (!newsess)
|
||||
newsess = find_channel (sess->server, nick);
|
||||
if (newsess)
|
||||
if (newsess && !sess->server->have_echo_message)
|
||||
{
|
||||
message_tags_data no_tags = MESSAGE_TAGS_DATA_INIT;
|
||||
|
||||
@@ -3462,28 +3593,45 @@ cmd_server (struct session *sess, char *tbuf, char *word[], char *word_eol[])
|
||||
int use_ssl = FALSE;
|
||||
#endif
|
||||
int is_url = TRUE;
|
||||
int no_proxy = FALSE;
|
||||
server *serv = sess->server;
|
||||
ircnet *net = NULL;
|
||||
|
||||
while (TRUE)
|
||||
{
|
||||
#ifdef USE_OPENSSL
|
||||
/* BitchX uses -ssl, mIRC uses -e, let's support both */
|
||||
if (g_strcmp0 (word[2], "-ssl") == 0 || g_strcmp0 (word[2], "-e") == 0)
|
||||
{
|
||||
use_ssl = TRUE;
|
||||
offset++; /* args move up by 1 word */
|
||||
}
|
||||
else if (g_strcmp0 (word[2], "-ssl-noverify") == 0)
|
||||
{
|
||||
use_ssl = TRUE;
|
||||
use_ssl_noverify = TRUE;
|
||||
offset++; /* args move up by 1 word */
|
||||
}
|
||||
else if (g_strcmp0 (word[2], "-insecure") == 0)
|
||||
{
|
||||
use_ssl = FALSE;
|
||||
offset++; /* args move up by 1 word */
|
||||
}
|
||||
if (g_strcmp0 (word[2 + offset], "-ssl") == 0 || g_strcmp0 (word[2 + offset], "-e") == 0)
|
||||
{
|
||||
use_ssl = TRUE;
|
||||
use_ssl_noverify = FALSE;
|
||||
offset++;
|
||||
}
|
||||
else if (g_strcmp0 (word[2 + offset], "-ssl-noverify") == 0)
|
||||
{
|
||||
use_ssl = TRUE;
|
||||
use_ssl_noverify = TRUE;
|
||||
offset++;
|
||||
}
|
||||
else if (g_strcmp0 (word[2 + offset], "-insecure") == 0)
|
||||
{
|
||||
use_ssl = FALSE;
|
||||
use_ssl_noverify = FALSE;
|
||||
offset++;
|
||||
}
|
||||
else
|
||||
#endif
|
||||
if (g_strcmp0 (word[2 + offset], "-noproxy") == 0)
|
||||
{
|
||||
no_proxy = TRUE;
|
||||
offset++;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
serv->dont_use_proxy = no_proxy;
|
||||
|
||||
if (!parse_irc_url (word[2 + offset], &server_name, &port, &channel, &key, &use_ssl))
|
||||
{
|
||||
@@ -3584,10 +3732,18 @@ cmd_servchan (struct session *sess, char *tbuf, char *word[],
|
||||
{
|
||||
int offset = 0;
|
||||
|
||||
while (TRUE)
|
||||
{
|
||||
#ifdef USE_OPENSSL
|
||||
if (g_strcmp0 (word[2], "-ssl") == 0 || g_strcmp0 (word[2], "-ssl-noverify") == 0 || g_strcmp0 (word[2], "-insecure") == 0)
|
||||
offset++;
|
||||
if (g_strcmp0 (word[2 + offset], "-ssl") == 0 || g_strcmp0 (word[2 + offset], "-ssl-noverify") == 0 || g_strcmp0 (word[2 + offset], "-insecure") == 0)
|
||||
offset++;
|
||||
else
|
||||
#endif
|
||||
if (g_strcmp0 (word[2 + offset], "-noproxy") == 0)
|
||||
offset++;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
if (*word[4 + offset])
|
||||
{
|
||||
@@ -4114,22 +4270,24 @@ const struct commands xc_cmds[] = {
|
||||
#endif
|
||||
{"RECV", cmd_recv, 1, 0, 1, N_("RECV <text>, send raw data to ZoiteChat, as if it was received from the IRC server")},
|
||||
{"RELOAD", cmd_reload, 0, 0, 1, N_("RELOAD <name>, reloads a plugin or script")},
|
||||
{"REPLY", cmd_reply, 0, 0, 1, N_("REPLY <msgid> <message>, sends a reply-tagged message to the current channel or dialog")},
|
||||
{"SAY", cmd_say, 0, 0, 1,
|
||||
N_("SAY <text>, sends the text to the object in the current window")},
|
||||
{"SEND", cmd_send, 0, 0, 1, N_("SEND <nick> [<file>]")},
|
||||
{"TYPING", cmd_typing, 0, 0, 1, N_("TYPING [active|paused|done] [target], sends a typing notification")},
|
||||
#ifdef USE_OPENSSL
|
||||
{"SERVCHAN", cmd_servchan, 0, 0, 1,
|
||||
N_("SERVCHAN [-insecure|-ssl|-ssl-noverify] <host> <port> <channel>, connects and joins a channel using ssl unless otherwise specified")},
|
||||
N_("SERVCHAN [-noproxy] [-insecure|-ssl|-ssl-noverify] <host> <port> <channel>, connects and joins a channel using ssl unless otherwise specified")},
|
||||
#else
|
||||
{"SERVCHAN", cmd_servchan, 0, 0, 1,
|
||||
N_("SERVCHAN <host> <port> <channel>, connects and joins a channel")},
|
||||
N_("SERVCHAN [-noproxy] <host> <port> <channel>, connects and joins a channel")},
|
||||
#endif
|
||||
#ifdef USE_OPENSSL
|
||||
{"SERVER", cmd_server, 0, 0, 1,
|
||||
N_("SERVER [-insecure|-ssl|-ssl-noverify] <host> [<port>] [<password>], connects to a server using ssl unless otherwise specified, the default port is 6697 for ssl connections and 6667 for insecure connections")},
|
||||
N_("SERVER [-noproxy] [-insecure|-ssl|-ssl-noverify] <host> [<port>] [<password>], connects to a server using ssl unless otherwise specified, the default port is 6697 for ssl connections and 6667 for insecure connections")},
|
||||
#else
|
||||
{"SERVER", cmd_server, 0, 0, 1,
|
||||
N_("SERVER <host> [<port>] [<password>], connects to a server, the default port is 6667")},
|
||||
N_("SERVER [-noproxy] <host> [<port>] [<password>], connects to a server, the default port is 6667")},
|
||||
#endif
|
||||
{"SET", cmd_set, 0, 0, 1, N_("SET [-e] [-off|-on] [-quiet] <variable> [<value>]")},
|
||||
{"SETCURSOR", cmd_setcursor, 0, 0, 1, N_("SETCURSOR [-|+]<position>, reposition the cursor in the inputbox")},
|
||||
@@ -4649,8 +4807,9 @@ handle_say (session *sess, char *text, int check_spch)
|
||||
|
||||
while ((split_text = split_up_text (sess, text + offset, cmd_length, split_text)))
|
||||
{
|
||||
inbound_chanmsg (sess->server, sess, sess->channel, sess->server->nick,
|
||||
split_text, TRUE, FALSE, &no_tags);
|
||||
if (!sess->server->have_echo_message)
|
||||
inbound_chanmsg (sess->server, sess, sess->channel, sess->server->nick,
|
||||
split_text, TRUE, FALSE, &no_tags);
|
||||
sess->server->p_message (sess->server, sess->channel, split_text);
|
||||
|
||||
if (*split_text)
|
||||
@@ -4659,7 +4818,8 @@ handle_say (session *sess, char *text, int check_spch)
|
||||
g_free (split_text);
|
||||
}
|
||||
|
||||
inbound_chanmsg (sess->server, sess, sess->channel, sess->server->nick,
|
||||
if (!sess->server->have_echo_message)
|
||||
inbound_chanmsg (sess->server, sess, sess->channel, sess->server->nick,
|
||||
text + offset, TRUE, FALSE, &no_tags);
|
||||
sess->server->p_message (sess->server, sess->channel, text + offset);
|
||||
} else
|
||||
@@ -4708,9 +4868,9 @@ command_insert_vars (session *sess, char *cmd)
|
||||
break;
|
||||
|
||||
case 'p':
|
||||
if (mynet->pass)
|
||||
if (sess->server->password[0])
|
||||
{
|
||||
g_string_append (expanded, mynet->pass);
|
||||
g_string_append (expanded, sess->server->password);
|
||||
}
|
||||
cmd++;
|
||||
break;
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
#ifdef WIN32
|
||||
#include <io.h>
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
@@ -56,6 +57,36 @@ typedef struct session zoitechat_context;
|
||||
#endif
|
||||
|
||||
#define DEBUG(x) {x;}
|
||||
#ifdef WIN32
|
||||
|
||||
/* Python >= 3.9 dropped support for Windows versions before 10, so those
|
||||
* systems get a Python 3.8 build of the plugin instead. RtlGetVersion is
|
||||
* used because GetVersionEx reports a version capped by the compatibility
|
||||
* manifest rather than the real one. */
|
||||
static gboolean
|
||||
plugin_windows_needs_python38 (void)
|
||||
{
|
||||
typedef LONG (WINAPI *rtl_get_version_func) (PRTL_OSVERSIONINFOW);
|
||||
rtl_get_version_func rtl_get_version;
|
||||
RTL_OSVERSIONINFOW version;
|
||||
HMODULE ntdll;
|
||||
|
||||
ntdll = GetModuleHandleW (L"ntdll.dll");
|
||||
if (ntdll == NULL)
|
||||
return FALSE;
|
||||
|
||||
rtl_get_version = (rtl_get_version_func) GetProcAddress (ntdll, "RtlGetVersion");
|
||||
if (rtl_get_version == NULL)
|
||||
return FALSE;
|
||||
|
||||
memset (&version, 0, sizeof (version));
|
||||
version.dwOSVersionInfoSize = sizeof (version);
|
||||
if (rtl_get_version (&version) != 0)
|
||||
return FALSE;
|
||||
|
||||
return version.dwMajorVersion < 10;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* crafted to be an even 32 bytes */
|
||||
struct _zoitechat_hook
|
||||
@@ -491,7 +522,10 @@ plugin_auto_load (session *sess)
|
||||
for_files (lib_dir, "hcfishlim.dll", plugin_auto_load_cb);
|
||||
for_files(lib_dir, "hclua.dll", plugin_auto_load_cb);
|
||||
for_files (lib_dir, "hcperl.dll", plugin_auto_load_cb);
|
||||
for_files (lib_dir, "hcpython3.dll", plugin_auto_load_cb);
|
||||
if (plugin_windows_needs_python38 ())
|
||||
for_files (lib_dir, "hcpython38.dll", plugin_auto_load_cb);
|
||||
else
|
||||
for_files (lib_dir, "hcpython3.dll", plugin_auto_load_cb);
|
||||
for_files (lib_dir, "hcupd.dll", plugin_auto_load_cb);
|
||||
for_files (lib_dir, "hcsysinfo.dll", plugin_auto_load_cb);
|
||||
#else
|
||||
|
||||
@@ -362,6 +362,18 @@ irc_message (server *serv, char *channel, char *text)
|
||||
tcp_sendf (serv, "PRIVMSG %s :%s\r\n", channel, text);
|
||||
}
|
||||
|
||||
static void
|
||||
irc_message_tagged (server *serv, char *tags, char *channel, char *text)
|
||||
{
|
||||
tcp_sendf (serv, "@%s PRIVMSG %s :%s\r\n", tags, channel, text);
|
||||
}
|
||||
|
||||
static void
|
||||
irc_tagmsg (server *serv, char *tags, char *target)
|
||||
{
|
||||
tcp_sendf (serv, "@%s TAGMSG %s\r\n", tags, target);
|
||||
}
|
||||
|
||||
static void
|
||||
irc_action (server *serv, char *channel, char *act)
|
||||
{
|
||||
@@ -425,7 +437,7 @@ static int
|
||||
irc_raw (server *serv, char *raw)
|
||||
{
|
||||
int len;
|
||||
char tbuf[4096];
|
||||
char tbuf[8704];
|
||||
if (*raw)
|
||||
{
|
||||
len = strlen (raw);
|
||||
@@ -1009,6 +1021,40 @@ process_numeric (session * sess, int n,
|
||||
|
||||
/* handle named messages that starts with a ':' */
|
||||
|
||||
|
||||
static void
|
||||
emit_standard_reply (session *sess, const char *type, char *command, char *code, char *text, const message_tags_data *tags_data)
|
||||
{
|
||||
int is_global = g_strcmp0 (command, "*") == 0;
|
||||
|
||||
if (!text)
|
||||
text = "";
|
||||
if (*text == ':')
|
||||
text++;
|
||||
|
||||
if (!strcmp (type, "FAIL"))
|
||||
{
|
||||
if (is_global)
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_FAIL, sess, code, text, NULL, NULL, NULL, tags_data->timestamp);
|
||||
else
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_FAILCMD, sess, command, code, text, NULL, NULL, tags_data->timestamp);
|
||||
}
|
||||
else if (!strcmp (type, "WARN"))
|
||||
{
|
||||
if (is_global)
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_WARN, sess, code, text, NULL, NULL, NULL, tags_data->timestamp);
|
||||
else
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_WARNCMD, sess, command, code, text, NULL, NULL, tags_data->timestamp);
|
||||
}
|
||||
else if (!strcmp (type, "NOTE"))
|
||||
{
|
||||
if (is_global)
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_NOTE, sess, code, text, NULL, NULL, NULL, tags_data->timestamp);
|
||||
else
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_NOTECMD, sess, command, code, text, NULL, NULL, tags_data->timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
process_named_msg (session *sess, char *type, char *word[], char *word_eol[],
|
||||
const message_tags_data *tags_data)
|
||||
@@ -1322,7 +1368,20 @@ process_named_msg (session *sess, char *type, char *word[], char *word_eol[],
|
||||
{
|
||||
if (ignore_check (word[1], IG_PRIV))
|
||||
return;
|
||||
inbound_privmsg (serv, nick, ip, text, tags_data->identified, tags_data);
|
||||
if (serv->have_echo_message && !serv->p_cmp (nick, serv->nick))
|
||||
{
|
||||
session *target_sess = find_dialog (serv, to);
|
||||
|
||||
if (!target_sess)
|
||||
target_sess = find_channel (serv, to);
|
||||
if (target_sess)
|
||||
inbound_chanmsg (serv, target_sess, target_sess->channel, nick, text, TRUE, tags_data->identified, tags_data);
|
||||
else if (serv->front_session)
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_MSGSEND, serv->front_session, to, text, NULL, NULL, 0, tags_data->timestamp);
|
||||
} else
|
||||
{
|
||||
inbound_privmsg (serv, nick, ip, text, tags_data->identified, tags_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1335,6 +1394,29 @@ process_named_msg (session *sess, char *type, char *word[], char *word_eol[],
|
||||
tags_data);
|
||||
return;
|
||||
|
||||
case WORDL('T','A','G','M'):
|
||||
if (tags_data->typing)
|
||||
{
|
||||
session *typing_sess = NULL;
|
||||
char *to = word[3];
|
||||
|
||||
if (is_channel (serv, to))
|
||||
typing_sess = find_channel (serv, to);
|
||||
else if (!serv->p_cmp (to, serv->nick))
|
||||
typing_sess = find_dialog (serv, nick);
|
||||
if (!typing_sess)
|
||||
typing_sess = sess;
|
||||
if (!serv->p_cmp (nick, serv->nick))
|
||||
return;
|
||||
if (!strcmp (tags_data->typing, "done"))
|
||||
fe_set_typing (typing_sess, nick, "done");
|
||||
else if (!strcmp (tags_data->typing, "paused"))
|
||||
fe_set_typing (typing_sess, nick, "paused");
|
||||
else if (!strcmp (tags_data->typing, "active"))
|
||||
fe_set_typing (typing_sess, nick, "active");
|
||||
}
|
||||
return;
|
||||
|
||||
case WORDL('W','A','L','L'):
|
||||
text = word_eol[3];
|
||||
if (*text == ':')
|
||||
@@ -1431,6 +1513,11 @@ process_named_servermsg (session *sess, char *buf, char *rawname, char *word_eol
|
||||
inbound_sasl_authenticate (sess->server, word_eol[2]);
|
||||
return;
|
||||
}
|
||||
if (!strncmp (buf, "FAIL ", 5) || !strncmp (buf, "WARN ", 5) || !strncmp (buf, "NOTE ", 5))
|
||||
{
|
||||
emit_standard_reply (sess, word_eol[1], word_eol[2], word_eol[3], word_eol[4], tags_data);
|
||||
return;
|
||||
}
|
||||
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_SERVTEXT, sess, buf, sess->server->servername,
|
||||
rawname, NULL, 0, tags_data->timestamp);
|
||||
@@ -1533,39 +1620,130 @@ handle_message_tag_time (const char *time, message_tags_data *tags_data)
|
||||
*
|
||||
* See http://ircv3.atheme.org/specification/message-tags-3.2
|
||||
*/
|
||||
static char *
|
||||
message_tag_unescape (const char *value)
|
||||
{
|
||||
GString *out;
|
||||
const char *p;
|
||||
|
||||
if (!*value)
|
||||
return NULL;
|
||||
|
||||
out = g_string_sized_new (strlen (value));
|
||||
|
||||
for (p = value; *p; p++)
|
||||
{
|
||||
if (*p != '\\')
|
||||
{
|
||||
g_string_append_c (out, *p);
|
||||
continue;
|
||||
}
|
||||
|
||||
p++;
|
||||
if (!*p)
|
||||
break;
|
||||
|
||||
switch (*p)
|
||||
{
|
||||
case ':':
|
||||
g_string_append_c (out, ';');
|
||||
break;
|
||||
case 's':
|
||||
g_string_append_c (out, ' ');
|
||||
break;
|
||||
case '\\':
|
||||
g_string_append_c (out, '\\');
|
||||
break;
|
||||
case 'r':
|
||||
g_string_append_c (out, '\r');
|
||||
break;
|
||||
case 'n':
|
||||
g_string_append_c (out, '\n');
|
||||
break;
|
||||
default:
|
||||
g_string_append_c (out, *p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
value = out->str;
|
||||
if (!g_utf8_validate (value, -1, NULL))
|
||||
{
|
||||
g_string_free (out, TRUE);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return g_string_free (out, FALSE);
|
||||
}
|
||||
|
||||
static void
|
||||
handle_message_tags (server *serv, const char *tags_str,
|
||||
message_tags_data *tags_data)
|
||||
{
|
||||
char **tags;
|
||||
char *time = NULL;
|
||||
int i;
|
||||
|
||||
/* FIXME We might want to avoid the allocation overhead here since
|
||||
* this might be called for every message from the server.
|
||||
*/
|
||||
tags = g_strsplit (tags_str, ";", 0);
|
||||
|
||||
for (i=0; tags[i]; i++)
|
||||
for (i = 0; tags[i]; i++)
|
||||
{
|
||||
char *key = tags[i];
|
||||
char *value = strchr (tags[i], '=');
|
||||
char *raw_value = strchr (tags[i], '=');
|
||||
char *value = NULL;
|
||||
|
||||
if (!value)
|
||||
if (!*key)
|
||||
continue;
|
||||
|
||||
*value = '\0';
|
||||
value++;
|
||||
if (raw_value)
|
||||
{
|
||||
*raw_value = '\0';
|
||||
raw_value++;
|
||||
value = message_tag_unescape (raw_value);
|
||||
}
|
||||
|
||||
if (serv->have_account_tag && !strcmp (key, "account"))
|
||||
tags_data->account = g_strdup (value);
|
||||
|
||||
if (serv->have_idmsg && strcmp (key, "solanum.chat/identified"))
|
||||
{
|
||||
g_free (tags_data->account);
|
||||
tags_data->account = value;
|
||||
value = NULL;
|
||||
}
|
||||
else if (serv->have_idmsg && !strcmp (key, "solanum.chat/identified"))
|
||||
{
|
||||
tags_data->identified = TRUE;
|
||||
}
|
||||
else if (serv->have_server_time && !strcmp (key, "time"))
|
||||
{
|
||||
g_free (time);
|
||||
time = value;
|
||||
value = NULL;
|
||||
}
|
||||
else if (!strcmp (key, "msgid"))
|
||||
{
|
||||
g_free (tags_data->msgid);
|
||||
tags_data->msgid = value;
|
||||
value = NULL;
|
||||
}
|
||||
else if (!strcmp (key, "+reply"))
|
||||
{
|
||||
g_free (tags_data->reply);
|
||||
tags_data->reply = value;
|
||||
value = NULL;
|
||||
}
|
||||
else if (!strcmp (key, "+typing"))
|
||||
{
|
||||
g_free (tags_data->typing);
|
||||
tags_data->typing = value;
|
||||
value = NULL;
|
||||
}
|
||||
|
||||
if (serv->have_server_time && !strcmp (key, "time"))
|
||||
handle_message_tag_time (value, tags_data);
|
||||
g_free (value);
|
||||
}
|
||||
|
||||
|
||||
if (time)
|
||||
handle_message_tag_time (time, tags_data);
|
||||
|
||||
g_free (time);
|
||||
g_strfreev (tags);
|
||||
}
|
||||
|
||||
@@ -1667,6 +1845,9 @@ void
|
||||
message_tags_data_free (message_tags_data *tags_data)
|
||||
{
|
||||
g_clear_pointer (&tags_data->account, g_free);
|
||||
g_clear_pointer (&tags_data->msgid, g_free);
|
||||
g_clear_pointer (&tags_data->reply, g_free);
|
||||
g_clear_pointer (&tags_data->typing, g_free);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1696,6 +1877,8 @@ proto_fill_her_up (server *serv)
|
||||
serv->p_set_back = irc_set_back;
|
||||
serv->p_set_away = irc_set_away;
|
||||
serv->p_message = irc_message;
|
||||
serv->p_message_tagged = irc_message_tagged;
|
||||
serv->p_tagmsg = irc_tagmsg;
|
||||
serv->p_action = irc_action;
|
||||
serv->p_notice = irc_notice;
|
||||
serv->p_topic = irc_topic;
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
NULL, /* account name */ \
|
||||
FALSE, /* identified to nick */ \
|
||||
(time_t)0, /* timestamp */ \
|
||||
NULL, \
|
||||
NULL, \
|
||||
NULL, \
|
||||
}
|
||||
|
||||
#define STRIP_COLON(word, word_eol, idx) (word)[(idx)][0] == ':' ? (word_eol)[(idx)]+1 : (word)[(idx)]
|
||||
@@ -41,6 +44,9 @@ typedef struct
|
||||
char *account;
|
||||
gboolean identified;
|
||||
time_t timestamp;
|
||||
char *msgid;
|
||||
char *reply;
|
||||
char *typing;
|
||||
} message_tags_data;
|
||||
|
||||
void message_tags_data_free (message_tags_data *tags_data);
|
||||
|
||||
@@ -109,6 +109,58 @@ static void server_disconnect (session * sess, int sendquit, int err);
|
||||
static int server_cleanup (server * serv);
|
||||
static void server_connect (server *serv, char *hostname, int port, int no_login);
|
||||
|
||||
typedef struct
|
||||
{
|
||||
server *serv;
|
||||
char hostname[128];
|
||||
int port;
|
||||
} reconnect_password_prompt;
|
||||
|
||||
static gboolean
|
||||
server_login_uses_password_sasl (int logintype)
|
||||
{
|
||||
return logintype == LOGIN_SASL ||
|
||||
logintype == LOGIN_SASL_SCRAM_SHA_1 ||
|
||||
logintype == LOGIN_SASL_SCRAM_SHA_256 ||
|
||||
logintype == LOGIN_SASL_SCRAM_SHA_512;
|
||||
}
|
||||
|
||||
static void
|
||||
server_reconnect_password_cb (int cancel, char *text, void *userdata)
|
||||
{
|
||||
reconnect_password_prompt *prompt = userdata;
|
||||
|
||||
if (!cancel && is_server (prompt->serv) && !prompt->serv->connected && !prompt->serv->connecting && prompt->serv->server_session)
|
||||
{
|
||||
prompt->serv->password[0] = 0;
|
||||
if (text && *text)
|
||||
safe_strcpy (prompt->serv->password, text, sizeof (prompt->serv->password));
|
||||
server_connect (prompt->serv, prompt->hostname, prompt->port, FALSE);
|
||||
}
|
||||
|
||||
g_free (prompt);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
server_prompt_reconnect_password (server *serv)
|
||||
{
|
||||
reconnect_password_prompt *prompt;
|
||||
char *msg;
|
||||
ircnet *net = serv->network;
|
||||
|
||||
if (!net || !(net->flags & FLAG_PROMPT_PASSWORD) || !server_login_uses_password_sasl (serv->loginmethod))
|
||||
return FALSE;
|
||||
|
||||
prompt = g_new0 (reconnect_password_prompt, 1);
|
||||
prompt->serv = serv;
|
||||
safe_strcpy (prompt->hostname, serv->hostname, sizeof (prompt->hostname));
|
||||
prompt->port = serv->port;
|
||||
msg = g_strdup_printf (_("Enter SASL password for %s:"), net->name ? net->name : _("this network"));
|
||||
fe_get_password (msg, server_reconnect_password_cb, prompt);
|
||||
g_free (msg);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
write_error (char *message, GError **error)
|
||||
{
|
||||
@@ -522,7 +574,7 @@ ssl_cb_verify (int ok, X509_STORE_CTX * ctx)
|
||||
X509 *current_cert = X509_STORE_CTX_get_current_cert (ctx);
|
||||
|
||||
if (!current_cert)
|
||||
return TRUE;
|
||||
return ok;
|
||||
|
||||
X509_NAME_oneline (X509_get_subject_name (current_cert),
|
||||
subject, sizeof (subject));
|
||||
@@ -534,13 +586,24 @@ ssl_cb_verify (int ok, X509_STORE_CTX * ctx)
|
||||
g_snprintf (buf, sizeof (buf), "* Issuer: %s", issuer);
|
||||
EMIT_SIGNAL (XP_TE_SSLMESSAGE, g_sess, buf, NULL, NULL, NULL, 0);
|
||||
|
||||
return TRUE;
|
||||
if (!ok)
|
||||
{
|
||||
int err = X509_STORE_CTX_get_error (ctx);
|
||||
g_snprintf (buf, sizeof (buf), "* Verify E: %s (%d)",
|
||||
X509_verify_cert_error_string (err), err);
|
||||
EMIT_SIGNAL (XP_TE_SSLMESSAGE, g_sess, buf, NULL, NULL, NULL, 0);
|
||||
|
||||
if (g_sess && g_sess->server->accept_invalid_cert)
|
||||
return 1;
|
||||
}
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
static int
|
||||
ssl_do_connect (server * serv)
|
||||
{
|
||||
char buf[256]; // ERR_error_string() MUST have this size
|
||||
char buf[256];
|
||||
|
||||
g_sess = serv->server_session;
|
||||
|
||||
@@ -559,9 +622,10 @@ ssl_do_connect (server * serv)
|
||||
if (SSL_connect (serv->ssl) <= 0)
|
||||
{
|
||||
char err_buf[128];
|
||||
int err;
|
||||
int err, ssl_err;
|
||||
|
||||
g_sess = NULL;
|
||||
ssl_err = SSL_get_error (serv->ssl, -1);
|
||||
if ((err = ERR_get_error ()) > 0)
|
||||
{
|
||||
ERR_error_string (err, err_buf);
|
||||
@@ -571,6 +635,8 @@ ssl_do_connect (server * serv)
|
||||
|
||||
if (ERR_GET_REASON (err) == SSL_R_WRONG_VERSION_NUMBER)
|
||||
PrintText (serv->server_session, _("Are you sure this is a SSL capable server and port?\n"));
|
||||
else if (ssl_err == SSL_ERROR_SSL)
|
||||
EMIT_SIGNAL (XP_TE_SSLMESSAGE, serv->server_session, "* TLS handshake rejected by protocol/certificate/cipher policy", NULL, NULL, NULL, 0);
|
||||
|
||||
server_cleanup (serv);
|
||||
|
||||
@@ -649,30 +715,18 @@ ssl_do_connect (server * serv)
|
||||
int hostname_err;
|
||||
if ((hostname_err = _SSL_check_hostname(cert, serv->hostname)) != 0)
|
||||
{
|
||||
g_snprintf (buf, sizeof (buf), "* Verify E: Failed to validate hostname? (%d)%s",
|
||||
hostname_err, serv->accept_invalid_cert ? " -- Ignored" : "");
|
||||
if (serv->accept_invalid_cert)
|
||||
EMIT_SIGNAL (XP_TE_SSLMESSAGE, serv->server_session, buf, NULL, NULL, NULL, 0);
|
||||
else
|
||||
g_snprintf (buf, sizeof (buf), "* Verify E: Failed to validate hostname (%d)",
|
||||
hostname_err);
|
||||
EMIT_SIGNAL (XP_TE_SSLMESSAGE, serv->server_session, buf, NULL, NULL, NULL, 0);
|
||||
if (!serv->accept_invalid_cert)
|
||||
goto conn_fail;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY:
|
||||
case X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE:
|
||||
case X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT:
|
||||
case X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN:
|
||||
case X509_V_ERR_CERT_HAS_EXPIRED:
|
||||
if (serv->accept_invalid_cert)
|
||||
{
|
||||
g_snprintf (buf, sizeof (buf), "* Verify E: %s.? (%d) -- Ignored",
|
||||
X509_verify_cert_error_string (verify_error),
|
||||
verify_error);
|
||||
EMIT_SIGNAL (XP_TE_SSLMESSAGE, serv->server_session, buf, NULL, NULL,
|
||||
NULL, 0);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
if (serv->accept_invalid_cert)
|
||||
break;
|
||||
|
||||
g_snprintf (buf, sizeof (buf), "%s.? (%d)",
|
||||
X509_verify_cert_error_string (verify_error),
|
||||
verify_error);
|
||||
@@ -732,7 +786,8 @@ timeout_auto_reconnect (server *serv)
|
||||
serv->recondelay_tag = 0;
|
||||
if (!serv->connected && !serv->connecting && serv->server_session)
|
||||
{
|
||||
server_connect (serv, serv->hostname, serv->port, FALSE);
|
||||
if (!server_prompt_reconnect_password (serv))
|
||||
server_connect (serv, serv->hostname, serv->port, FALSE);
|
||||
}
|
||||
}
|
||||
return 0; /* returning 0 should remove the timeout handler */
|
||||
@@ -930,7 +985,7 @@ server_read_child (GIOChannel *source, GIOCondition condition, server *serv)
|
||||
break;
|
||||
case '5': /* prefs ip discovered */
|
||||
waitline2 (source, tbuf, sizeof tbuf);
|
||||
prefs.local_ip = inet_addr (tbuf);
|
||||
net_parse_ipv4 (tbuf, &prefs.local_ip);
|
||||
break;
|
||||
case '7': /* prefs.hex_net_bind_host resolve failed */
|
||||
sprintf (outbuf,
|
||||
@@ -1104,7 +1159,7 @@ traverse_socks (int print_fd, int sok, char *serverAddr, int port)
|
||||
sc.version = 4;
|
||||
sc.type = 1;
|
||||
sc.port = htons (port);
|
||||
sc.address = inet_addr (serverAddr);
|
||||
net_parse_ipv4 (serverAddr, &sc.address);
|
||||
g_strlcpy (sc.username, prefs.hex_irc_user_name, sizeof (sc.username));
|
||||
|
||||
send (sok, (char *) &sc, 8 + strlen (sc.username) + 1, 0);
|
||||
@@ -1819,6 +1874,7 @@ void
|
||||
server_set_defaults (server *serv)
|
||||
{
|
||||
g_free (serv->chantypes);
|
||||
g_clear_pointer (&serv->clienttagdeny, g_free);
|
||||
g_free (serv->chanmodes);
|
||||
g_free (serv->nick_prefixes);
|
||||
g_free (serv->nick_modes);
|
||||
@@ -1853,6 +1909,8 @@ server_set_defaults (server *serv)
|
||||
serv->have_extjoin = FALSE;
|
||||
serv->have_account_tag = FALSE;
|
||||
serv->have_server_time = FALSE;
|
||||
serv->have_message_tags = FALSE;
|
||||
serv->have_echo_message = FALSE;
|
||||
serv->have_sasl = FALSE;
|
||||
serv->have_except = FALSE;
|
||||
serv->have_invite = FALSE;
|
||||
@@ -1983,6 +2041,7 @@ server_free (server *serv)
|
||||
g_free (serv->nick_prefixes);
|
||||
g_free (serv->chanmodes);
|
||||
g_free (serv->chantypes);
|
||||
g_free (serv->clienttagdeny);
|
||||
g_free (serv->bad_nick_prefixes);
|
||||
g_free (serv->last_away_reason);
|
||||
g_free (serv->encoding);
|
||||
|
||||
@@ -439,8 +439,99 @@ servlist_favchan_copy (favchannel *fav)
|
||||
return newfav;
|
||||
}
|
||||
|
||||
static void servlist_connect_with_password (session *sess, ircnet *net, gboolean join, const char *password);
|
||||
|
||||
typedef struct
|
||||
{
|
||||
session *sess;
|
||||
ircnet *net;
|
||||
gboolean join;
|
||||
} servlist_password_prompt;
|
||||
|
||||
static gboolean
|
||||
servlist_login_uses_password_sasl (int logintype)
|
||||
{
|
||||
return logintype == LOGIN_SASL ||
|
||||
logintype == LOGIN_SASL_SCRAM_SHA_1 ||
|
||||
logintype == LOGIN_SASL_SCRAM_SHA_256 ||
|
||||
logintype == LOGIN_SASL_SCRAM_SHA_512;
|
||||
}
|
||||
|
||||
static void servlist_prompt_sasl_password (servlist_password_prompt *prompt);
|
||||
|
||||
static void
|
||||
servlist_connect_without_password_cb (int value, void *userdata)
|
||||
{
|
||||
servlist_password_prompt *prompt = userdata;
|
||||
|
||||
if (value)
|
||||
{
|
||||
servlist_connect_with_password (prompt->sess, prompt->net, prompt->join, NULL);
|
||||
g_free (prompt);
|
||||
}
|
||||
else
|
||||
{
|
||||
servlist_prompt_sasl_password (prompt);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
servlist_connect_password_cb (int cancel, char *text, void *userdata)
|
||||
{
|
||||
servlist_password_prompt *prompt = userdata;
|
||||
char *msg;
|
||||
|
||||
if (cancel)
|
||||
{
|
||||
fe_serverlist_open (prompt->sess);
|
||||
g_free (prompt);
|
||||
return;
|
||||
}
|
||||
|
||||
if (text && *text)
|
||||
{
|
||||
servlist_connect_with_password (prompt->sess, prompt->net, prompt->join, text);
|
||||
g_free (prompt);
|
||||
return;
|
||||
}
|
||||
|
||||
msg = g_strdup_printf (_("Continue connecting to %s without a SASL password?"), prompt->net->name ? prompt->net->name : _("this network"));
|
||||
fe_get_bool (_("No SASL password"), msg, servlist_connect_without_password_cb, prompt);
|
||||
g_free (msg);
|
||||
}
|
||||
|
||||
static void
|
||||
servlist_prompt_sasl_password (servlist_password_prompt *prompt)
|
||||
{
|
||||
char *msg;
|
||||
|
||||
msg = g_strdup_printf (_("Enter SASL password for %s:"), prompt->net->name ? prompt->net->name : _("this network"));
|
||||
fe_get_password (msg, servlist_connect_password_cb, prompt);
|
||||
g_free (msg);
|
||||
}
|
||||
|
||||
void
|
||||
servlist_connect (session *sess, ircnet *net, gboolean join)
|
||||
{
|
||||
int logintype;
|
||||
servlist_password_prompt *prompt;
|
||||
|
||||
logintype = net && net->logintype ? net->logintype : LOGIN_DEFAULT_REAL;
|
||||
if (net && (net->flags & FLAG_PROMPT_PASSWORD) && servlist_login_uses_password_sasl (logintype))
|
||||
{
|
||||
prompt = g_new0 (servlist_password_prompt, 1);
|
||||
prompt->sess = sess;
|
||||
prompt->net = net;
|
||||
prompt->join = join;
|
||||
servlist_prompt_sasl_password (prompt);
|
||||
return;
|
||||
}
|
||||
|
||||
servlist_connect_with_password (sess, net, join, NULL);
|
||||
}
|
||||
|
||||
static void
|
||||
servlist_connect_with_password (session *sess, ircnet *net, gboolean join, const char *password)
|
||||
{
|
||||
ircserver *ircserv;
|
||||
GSList *list;
|
||||
@@ -487,7 +578,11 @@ servlist_connect (session *sess, ircnet *net, gboolean join)
|
||||
}
|
||||
|
||||
serv->password[0] = 0;
|
||||
if ((net->flags & FLAG_USE_KEYRING) && net->name)
|
||||
if (password && *password)
|
||||
{
|
||||
safe_strcpy (serv->password, password, sizeof (serv->password));
|
||||
}
|
||||
else if ((net->flags & FLAG_USE_KEYRING) && net->name)
|
||||
{
|
||||
char *stored_pass = secretstore_get_network_password (net->name);
|
||||
if (stored_pass && *stored_pass)
|
||||
|
||||
@@ -63,7 +63,8 @@ extern GSList *network_list;
|
||||
#define FLAG_ALLOW_INVALID 32
|
||||
#define FLAG_FAVORITE 64
|
||||
#define FLAG_USE_KEYRING 128
|
||||
#define FLAG_COUNT 8
|
||||
#define FLAG_PROMPT_PASSWORD 256
|
||||
#define FLAG_COUNT 9
|
||||
|
||||
/* Login methods. Use server password by default - if we had a NickServ password, it'd be set to 2 already by servlist_load() */
|
||||
#define LOGIN_DEFAULT_REAL LOGIN_PASS /* this is to set the default login method for unknown servers */
|
||||
|
||||
@@ -86,15 +86,17 @@ _SSL_context_init (void (*info_cb_func))
|
||||
|
||||
SSLeay_add_ssl_algorithms ();
|
||||
SSL_load_error_strings ();
|
||||
ctx = SSL_CTX_new (SSLv23_client_method ());
|
||||
ctx = SSL_CTX_new (TLS_client_method ());
|
||||
|
||||
SSL_CTX_set_session_cache_mode (ctx, SSL_SESS_CACHE_BOTH);
|
||||
SSL_CTX_set_timeout (ctx, 300);
|
||||
SSL_CTX_set_options (ctx, SSL_OP_NO_SSLv2|SSL_OP_NO_SSLv3
|
||||
|SSL_OP_NO_COMPRESSION
|
||||
SSL_CTX_set_options (ctx, SSL_OP_NO_COMPRESSION
|
||||
|SSL_OP_SINGLE_DH_USE|SSL_OP_SINGLE_ECDH_USE
|
||||
|SSL_OP_NO_TICKET
|
||||
|SSL_OP_CIPHER_SERVER_PREFERENCE);
|
||||
|SSL_OP_NO_RENEGOTIATION);
|
||||
SSL_CTX_set_min_proto_version (ctx, TLS1_2_VERSION);
|
||||
SSL_CTX_set_cipher_list (ctx, "HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!DSS");
|
||||
SSL_CTX_set_ciphersuites (ctx, "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256");
|
||||
|
||||
#if OPENSSL_VERSION_NUMBER >= 0x00908000L && !defined (OPENSSL_NO_COMP) /* workaround for OpenSSL 0.9.8 */
|
||||
sk_SSL_COMP_zero(SSL_COMP_get_compression_methods());
|
||||
@@ -152,7 +154,7 @@ int
|
||||
_SSL_get_cert_info (struct cert_info *cert_info, SSL * ssl)
|
||||
{
|
||||
X509 *peer_cert;
|
||||
X509_PUBKEY *key;
|
||||
const X509_PUBKEY *key;
|
||||
X509_ALGOR *algor = NULL;
|
||||
EVP_PKEY *peer_pkey;
|
||||
char notBefore[64];
|
||||
@@ -311,7 +313,7 @@ _SSL_socket (SSL_CTX *ctx, int sd)
|
||||
#else
|
||||
method = SSL_CTX_get_ssl_method (ctx);
|
||||
#endif
|
||||
if (method == SSLv23_client_method())
|
||||
if (method == TLS_client_method())
|
||||
SSL_set_connect_state (ssl);
|
||||
else
|
||||
SSL_set_accept_state(ssl);
|
||||
@@ -348,14 +350,13 @@ _SSL_close (SSL * ssl)
|
||||
{
|
||||
SSL_set_shutdown (ssl, SSL_SENT_SHUTDOWN | SSL_RECEIVED_SHUTDOWN);
|
||||
SSL_free (ssl);
|
||||
#if !defined(OPENSSL_VERSION_MAJOR) && OPENSSL_VERSION_NUMBER < 0x10100000L
|
||||
#ifdef HAVE_ERR_REMOVE_THREAD_STATE
|
||||
#if OPENSSL_VERSION_NUMBER >= 0x10000000L && OPENSSL_VERSION_NUMBER < 0x10100000L
|
||||
/* OpenSSL handles this itself in 1.1+ and this is a no-op */
|
||||
ERR_remove_thread_state (NULL);
|
||||
#endif
|
||||
#else
|
||||
ERR_remove_state (0);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Hostname validation code based on OpenBSD's libtls. */
|
||||
@@ -506,9 +507,12 @@ _SSL_check_subject_altname (X509 *cert, const char *host)
|
||||
static int
|
||||
_SSL_check_common_name (X509 *cert, const char *host)
|
||||
{
|
||||
X509_NAME *name;
|
||||
char *common_name = NULL;
|
||||
const X509_NAME *name;
|
||||
const X509_NAME_ENTRY *entry;
|
||||
const ASN1_STRING *common_name_asn1;
|
||||
const unsigned char *common_name;
|
||||
int common_name_len;
|
||||
int common_name_index;
|
||||
int rv = -1;
|
||||
GInetAddress *addr;
|
||||
|
||||
@@ -516,16 +520,27 @@ _SSL_check_common_name (X509 *cert, const char *host)
|
||||
if (name == NULL)
|
||||
return -1;
|
||||
|
||||
common_name_len = X509_NAME_get_text_by_NID (name, NID_commonName, NULL, 0);
|
||||
if (common_name_len < 0)
|
||||
common_name_index = X509_NAME_get_index_by_NID (name, NID_commonName, -1);
|
||||
if (common_name_index < 0)
|
||||
return -1;
|
||||
|
||||
common_name = g_malloc0 (common_name_len + 1);
|
||||
entry = X509_NAME_get_entry (name, common_name_index);
|
||||
if (entry == NULL)
|
||||
return -1;
|
||||
|
||||
X509_NAME_get_text_by_NID (name, NID_commonName, common_name, common_name_len + 1);
|
||||
common_name_asn1 = X509_NAME_ENTRY_get_data (entry);
|
||||
if (common_name_asn1 == NULL)
|
||||
return -1;
|
||||
|
||||
#ifdef HAVE_ASN1_STRING_GET0_DATA
|
||||
common_name = ASN1_STRING_get0_data (common_name_asn1);
|
||||
#else
|
||||
common_name = ASN1_STRING_data (common_name_asn1);
|
||||
#endif
|
||||
common_name_len = ASN1_STRING_length (common_name_asn1);
|
||||
|
||||
/* NUL bytes in CN? */
|
||||
if (common_name_len != (int)strlen(common_name))
|
||||
if (common_name_len != (int)strlen((const char *)common_name))
|
||||
{
|
||||
g_warning ("NUL byte in Common Name field, probably a malicious certificate.\n");
|
||||
rv = -2;
|
||||
@@ -538,18 +553,17 @@ _SSL_check_common_name (X509 *cert, const char *host)
|
||||
* We don't want to attempt wildcard matching against IP
|
||||
* addresses, so perform a simple comparison here.
|
||||
*/
|
||||
if (g_strcmp0 (common_name, host) == 0)
|
||||
if (g_strcmp0 ((const char *)common_name, host) == 0)
|
||||
rv = 0;
|
||||
else
|
||||
rv = -1;
|
||||
|
||||
g_object_unref (addr);
|
||||
}
|
||||
else if (_SSL_match_hostname (common_name, host) == 0)
|
||||
else if (_SSL_match_hostname ((const char *)common_name, host) == 0)
|
||||
rv = 0;
|
||||
|
||||
out:
|
||||
g_free(common_name);
|
||||
return rv;
|
||||
}
|
||||
|
||||
|
||||
@@ -381,9 +381,10 @@ test_archive_without_theme_reports_css_error (void)
|
||||
}
|
||||
archive_path = g_build_filename (tmp_root, "invalid-theme.zip", NULL);
|
||||
|
||||
command = g_strdup_printf ("cd %s && zip -qr %s .", archive_root, archive_path);
|
||||
command = g_strdup_printf ("sh -c \"cd '%s' && zip -qr '%s' .\"", archive_root, archive_path);
|
||||
g_assert_true (g_spawn_command_line_sync (command, NULL, NULL, NULL, NULL));
|
||||
g_free (command);
|
||||
g_assert_true (g_file_test (archive_path, G_FILE_TEST_IS_REGULAR));
|
||||
|
||||
g_assert_false (zoitechat_gtk3_theme_service_import (archive_path, &imported_id, &error));
|
||||
g_assert_null (imported_id);
|
||||
@@ -397,25 +398,31 @@ test_archive_without_theme_reports_css_error (void)
|
||||
}
|
||||
|
||||
static void
|
||||
test_import_rejects_theme_missing_index_theme (void)
|
||||
test_import_accepts_theme_missing_index_theme (void)
|
||||
{
|
||||
char *tmp_root;
|
||||
char *src_root;
|
||||
char *theme_root;
|
||||
char *imported_id = NULL;
|
||||
GError *error = NULL;
|
||||
ZoitechatGtk3Theme *found;
|
||||
|
||||
setup_test_xdir (&tmp_root);
|
||||
src_root = g_build_filename (tmp_root, "src", NULL);
|
||||
g_mkdir_with_parents (src_root, 0700);
|
||||
theme_root = make_theme_dir (src_root, "NoIndex", FALSE, FALSE);
|
||||
|
||||
g_assert_false (zoitechat_gtk3_theme_service_import (theme_root, &imported_id, &error));
|
||||
g_assert_null (imported_id);
|
||||
g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL);
|
||||
g_assert_nonnull (g_strstr_len (error->message, -1, "missing required index.theme"));
|
||||
g_assert_nonnull (g_strstr_len (error->message, -1, "NoIndex"));
|
||||
g_error_free (error);
|
||||
/* index.theme is optional for GTK3 CSS themes: gtk-3.x/gtk.css alone
|
||||
* makes a loadable theme. */
|
||||
g_assert_true (zoitechat_gtk3_theme_service_import (theme_root, &imported_id, &error));
|
||||
g_assert_no_error (error);
|
||||
g_assert_nonnull (imported_id);
|
||||
|
||||
found = zoitechat_gtk3_theme_find_by_id (imported_id);
|
||||
g_assert_nonnull (found);
|
||||
g_assert_true (g_str_has_suffix (found->path, "NoIndex"));
|
||||
zoitechat_gtk3_theme_free (found);
|
||||
g_free (imported_id);
|
||||
|
||||
g_free (theme_root);
|
||||
g_free (src_root);
|
||||
@@ -663,9 +670,10 @@ test_zip_import_nested_root (void)
|
||||
theme = make_theme_dir (nested, "Juno-ocean", TRUE, FALSE);
|
||||
archive_path = g_build_filename (tmp_root, "themes.zip", NULL);
|
||||
|
||||
command = g_strdup_printf ("cd %s && zip -qr %s .", zip_root, archive_path);
|
||||
command = g_strdup_printf ("sh -c \"cd '%s' && zip -qr '%s' .\"", zip_root, archive_path);
|
||||
g_assert_true (g_spawn_command_line_sync (command, NULL, NULL, NULL, NULL));
|
||||
g_free (command);
|
||||
g_assert_true (g_file_test (archive_path, G_FILE_TEST_IS_REGULAR));
|
||||
|
||||
g_assert_true (zoitechat_gtk3_theme_service_import (archive_path, &imported_id, NULL));
|
||||
found = zoitechat_gtk3_theme_find_by_id (imported_id);
|
||||
@@ -697,7 +705,7 @@ main (int argc, char **argv)
|
||||
g_test_add_func ("/gtk3-theme-service/zip-import-nested-root", test_zip_import_nested_root);
|
||||
g_test_add_func ("/gtk3-theme-service/invalid-archive-extract-error", test_invalid_archive_reports_extract_error);
|
||||
g_test_add_func ("/gtk3-theme-service/archive-without-theme-css-error", test_archive_without_theme_reports_css_error);
|
||||
g_test_add_func ("/gtk3-theme-service/import-missing-index-theme", test_import_rejects_theme_missing_index_theme);
|
||||
g_test_add_func ("/gtk3-theme-service/import-missing-index-theme", test_import_accepts_theme_missing_index_theme);
|
||||
g_test_add_func ("/gtk3-theme-service/import-missing-desktop-entry", test_import_rejects_index_without_desktop_entry);
|
||||
g_test_add_func ("/gtk3-theme-service/import-unresolved-inherits", test_import_rejects_unresolved_inherits);
|
||||
return g_test_run ();
|
||||
|
||||
@@ -1695,6 +1695,97 @@ pevent_load (char *filename)
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static gboolean
|
||||
text_event_uses_color (const char *text, int color)
|
||||
{
|
||||
const char *p;
|
||||
|
||||
if (!text || color < 0 || color > 31)
|
||||
return FALSE;
|
||||
|
||||
for (p = text; *p; p++)
|
||||
{
|
||||
int value;
|
||||
int digits;
|
||||
|
||||
if ((unsigned char) *p == 3)
|
||||
p++;
|
||||
else if (*p == '%' && p[1] == 'C')
|
||||
p += 2;
|
||||
else
|
||||
continue;
|
||||
|
||||
if (!g_ascii_isdigit (*p))
|
||||
{
|
||||
p--;
|
||||
continue;
|
||||
}
|
||||
|
||||
value = *p - '0';
|
||||
digits = 1;
|
||||
if (g_ascii_isdigit (p[1]))
|
||||
{
|
||||
value = value * 10 + p[1] - '0';
|
||||
digits = 2;
|
||||
}
|
||||
|
||||
if (value == color)
|
||||
return TRUE;
|
||||
|
||||
p += digits - 1;
|
||||
if (*p == ',' && g_ascii_isdigit (p[1]))
|
||||
{
|
||||
p++;
|
||||
value = *p - '0';
|
||||
if (g_ascii_isdigit (p[1]))
|
||||
{
|
||||
value = value * 10 + p[1] - '0';
|
||||
p++;
|
||||
}
|
||||
if (value == color)
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
char **
|
||||
text_color_event_names (int color, int *count)
|
||||
{
|
||||
GPtrArray *names;
|
||||
int found = 0;
|
||||
int i;
|
||||
|
||||
if (count)
|
||||
*count = 0;
|
||||
|
||||
names = g_ptr_array_new ();
|
||||
|
||||
for (i = 0; i < NUM_XP; i++)
|
||||
{
|
||||
const char *text = pntevts_text[i] ? pntevts_text[i] : te[i].def;
|
||||
|
||||
if (!text_event_uses_color (text, color))
|
||||
continue;
|
||||
|
||||
g_ptr_array_add (names, g_strdup (_(te[i].name)));
|
||||
found++;
|
||||
}
|
||||
|
||||
if (!found)
|
||||
{
|
||||
g_ptr_array_free (names, TRUE);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
g_ptr_array_add (names, NULL);
|
||||
if (count)
|
||||
*count = found;
|
||||
return (char **) g_ptr_array_free (names, FALSE);
|
||||
}
|
||||
|
||||
static void
|
||||
pevent_check_all_loaded (void)
|
||||
{
|
||||
@@ -1809,12 +1900,97 @@ format_event (session *sess, int index, char **args, char *o, gsize sizeofo, uns
|
||||
o[0] = 0;
|
||||
}
|
||||
|
||||
static char *
|
||||
text_event_without_hostmask_format (const char *format, int host_arg)
|
||||
{
|
||||
char token[3];
|
||||
const char *arg;
|
||||
const char *open;
|
||||
const char *close;
|
||||
const char *start;
|
||||
char *out;
|
||||
gsize prefix_len;
|
||||
|
||||
g_snprintf (token, sizeof (token), "$%d", host_arg);
|
||||
arg = strstr (format, token);
|
||||
if (!arg)
|
||||
return NULL;
|
||||
|
||||
open = arg;
|
||||
while (open > format && *open != '(' && *open != '\n')
|
||||
open--;
|
||||
|
||||
close = arg + strlen (token);
|
||||
while (*close && *close != ')' && *close != '\n')
|
||||
close++;
|
||||
|
||||
if (*open != '(' || *close != ')')
|
||||
return NULL;
|
||||
|
||||
start = open;
|
||||
if (start > format && start[-1] == ' ')
|
||||
start--;
|
||||
|
||||
prefix_len = start - format;
|
||||
out = g_malloc (prefix_len + strlen (close + 1) + 1);
|
||||
memcpy (out, format, prefix_len);
|
||||
strcpy (out + prefix_len, close + 1);
|
||||
return out;
|
||||
}
|
||||
|
||||
static void
|
||||
display_event_string (session *sess, int event, char **args, char *format,
|
||||
unsigned int stripcolor_args, time_t timestamp)
|
||||
{
|
||||
char *compiled;
|
||||
char *saved;
|
||||
char o[4096];
|
||||
int max_arg;
|
||||
|
||||
if (pevt_build_string (format, &compiled, &max_arg) != 0)
|
||||
return;
|
||||
|
||||
saved = pntevts[event];
|
||||
pntevts[event] = compiled;
|
||||
format_event (sess, event, args, o, sizeof (o), stripcolor_args);
|
||||
pntevts[event] = saved;
|
||||
g_free (compiled);
|
||||
|
||||
if (o[0])
|
||||
PrintTextTimeStamp (sess, o, timestamp);
|
||||
}
|
||||
|
||||
static void
|
||||
display_event (session *sess, int event, char **args,
|
||||
unsigned int stripcolor_args, time_t timestamp)
|
||||
{
|
||||
char o[4096];
|
||||
format_event (sess, event, args, o, sizeof (o), stripcolor_args);
|
||||
char *format;
|
||||
char *host;
|
||||
int host_arg;
|
||||
|
||||
if (prefs.hex_irc_hide_join_part_hostmask &&
|
||||
(event == XP_TE_JOIN || event == XP_TE_PART || event == XP_TE_PARTREASON))
|
||||
{
|
||||
host_arg = event == XP_TE_JOIN ? 3 : 2;
|
||||
format = text_event_without_hostmask_format (pntevts_text[event], host_arg);
|
||||
if (format)
|
||||
{
|
||||
display_event_string (sess, event, args, format, stripcolor_args, timestamp);
|
||||
g_free (format);
|
||||
return;
|
||||
}
|
||||
|
||||
host = args[host_arg];
|
||||
args[host_arg] = "";
|
||||
format_event (sess, event, args, o, sizeof (o), stripcolor_args);
|
||||
args[host_arg] = host;
|
||||
}
|
||||
else
|
||||
{
|
||||
format_event (sess, event, args, o, sizeof (o), stripcolor_args);
|
||||
}
|
||||
|
||||
if (o[0])
|
||||
PrintTextTimeStamp (sess, o, timestamp);
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ int pevt_build_string (const char *input, char **output, int *max_arg);
|
||||
int pevent_load (char *filename);
|
||||
void pevent_make_pntevts (void);
|
||||
int text_color_of (char *name);
|
||||
char **text_color_event_names (int color, int *count);
|
||||
void text_emit (int index, session *sess, char *a, char *b, char *c, char *d,
|
||||
time_t timestamp);
|
||||
int text_emit_by_name (char *name, session *sess, time_t timestamp,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,6 +31,7 @@ struct User
|
||||
char *servername;
|
||||
char *account;
|
||||
time_t lasttalk;
|
||||
time_t typing_time;
|
||||
unsigned int access; /* axs bit field */
|
||||
char prefix[2]; /* @ + % */
|
||||
unsigned int op:1;
|
||||
@@ -39,6 +40,7 @@ struct User
|
||||
unsigned int me:1;
|
||||
unsigned int away:1;
|
||||
unsigned int selected:1;
|
||||
unsigned int typing:2;
|
||||
};
|
||||
|
||||
#define USERACCESS_SIZE 12
|
||||
|
||||
@@ -380,6 +380,7 @@ lag_check (void)
|
||||
time_t now = time (0);
|
||||
time_t lag;
|
||||
time_t ping_age;
|
||||
unsigned long ping_timeout;
|
||||
|
||||
tim = make_ping_time ();
|
||||
|
||||
@@ -389,8 +390,11 @@ lag_check (void)
|
||||
if (serv->connected && serv->end_of_motd)
|
||||
{
|
||||
lag = now - serv->ping_recv;
|
||||
if (serv->lag_sent && prefs.hex_net_ping_timeout != 0 && lag > prefs.hex_net_ping_timeout && lag > 0)
|
||||
ping_timeout = (unsigned long) prefs.hex_net_ping_timeout * 1000;
|
||||
if (serv->lag_sent && prefs.hex_net_ping_timeout != 0
|
||||
&& tim - serv->lag_sent > ping_timeout)
|
||||
{
|
||||
lag = (tim - serv->lag_sent) / 1000;
|
||||
sprintf (tbuf, "%" G_GINT64_FORMAT, (gint64) lag);
|
||||
EMIT_SIGNAL (XP_TE_PINGTIMEOUT, serv->server_session, tbuf, NULL,
|
||||
NULL, NULL, 0);
|
||||
@@ -793,7 +797,13 @@ session_free (session *killsess)
|
||||
|
||||
send_quit_or_part (killsess);
|
||||
|
||||
if (killsess->typing_timeout_tag)
|
||||
fe_timeout_remove (killsess->typing_timeout_tag);
|
||||
if (killsess->typing_animation_tag)
|
||||
fe_timeout_remove (killsess->typing_animation_tag);
|
||||
|
||||
history_free (&killsess->history);
|
||||
reply_cache_free (killsess);
|
||||
g_free (killsess->topic);
|
||||
g_free (killsess->current_modes);
|
||||
|
||||
|
||||
@@ -133,6 +133,7 @@ struct zoitechatprefs
|
||||
unsigned int hex_gui_input_style;
|
||||
unsigned int hex_gui_join_dialog;
|
||||
unsigned int hex_gui_mode_buttons;
|
||||
unsigned int hex_gui_mode_buttons_inline;
|
||||
unsigned int hex_gui_quit_dialog;
|
||||
/* unsigned int hex_gui_single; */
|
||||
unsigned int hex_gui_slist_fav;
|
||||
@@ -148,6 +149,7 @@ struct zoitechatprefs
|
||||
unsigned int hex_gui_tab_sort;
|
||||
unsigned int hex_gui_tab_utils;
|
||||
unsigned int hex_gui_topicbar;
|
||||
unsigned int hex_gui_topicbar_multiline;
|
||||
unsigned int hex_gui_tray;
|
||||
unsigned int hex_gui_tray_away;
|
||||
unsigned int hex_gui_tray_blink;
|
||||
@@ -188,6 +190,7 @@ struct zoitechatprefs
|
||||
unsigned int hex_irc_conf_mode;
|
||||
unsigned int hex_irc_hidehost;
|
||||
unsigned int hex_irc_hide_nickchange;
|
||||
unsigned int hex_irc_hide_join_part_hostmask;
|
||||
unsigned int hex_irc_hide_version;
|
||||
unsigned int hex_irc_invisible;
|
||||
unsigned int hex_irc_logging;
|
||||
@@ -385,6 +388,14 @@ typedef enum {
|
||||
TAB_STATE_NEW_HILIGHT = (1 << 2),
|
||||
} tab_state_flags;
|
||||
|
||||
typedef struct reply_item
|
||||
{
|
||||
char *msgid;
|
||||
char *nick;
|
||||
char *text;
|
||||
time_t timestamp;
|
||||
} reply_item;
|
||||
|
||||
typedef struct session
|
||||
{
|
||||
/* Per-Channel Alerts */
|
||||
@@ -426,8 +437,17 @@ typedef struct session
|
||||
char *quitreason;
|
||||
char *topic;
|
||||
char *current_modes; /* free() me */
|
||||
GSList *reply_items;
|
||||
char *reply_msgid;
|
||||
char *reply_target;
|
||||
char *reply_nick;
|
||||
char *reply_text;
|
||||
|
||||
int mode_timeout_tag;
|
||||
int typing_timeout_tag;
|
||||
int typing_status;
|
||||
int typing_animation_tag;
|
||||
int typing_animation_frame;
|
||||
|
||||
struct session *lastlog_sess;
|
||||
struct nbexec *running_exec;
|
||||
@@ -491,6 +511,8 @@ typedef struct server
|
||||
void (*p_set_back)(struct server *);
|
||||
void (*p_set_away)(struct server *, char *reason);
|
||||
void (*p_message)(struct server *, char *channel, char *text);
|
||||
void (*p_message_tagged)(struct server *, char *tags, char *channel, char *text);
|
||||
void (*p_tagmsg)(struct server *, char *tags, char *target);
|
||||
void (*p_action)(struct server *, char *channel, char *act);
|
||||
void (*p_notice)(struct server *, char *channel, char *text);
|
||||
void (*p_topic)(struct server *, char *channel, char *topic);
|
||||
@@ -540,6 +562,7 @@ typedef struct server
|
||||
int loginmethod; /* see login_types[] */
|
||||
|
||||
char *chantypes; /* for 005 numeric - free me */
|
||||
char *clienttagdeny;
|
||||
char *chanmodes; /* for 005 numeric - free me */
|
||||
char *nick_prefixes; /* e.g. "*@%+" */
|
||||
char *nick_modes; /* e.g. "aohv" */
|
||||
@@ -602,6 +625,8 @@ typedef struct server
|
||||
unsigned int have_extjoin:1; /* cap extended-join */
|
||||
unsigned int have_account_tag:1; /* cap account-tag */
|
||||
unsigned int have_server_time:1; /* cap server-time */
|
||||
unsigned int have_message_tags:1;
|
||||
unsigned int have_echo_message:1;
|
||||
unsigned int have_sasl:1; /* SASL capability */
|
||||
unsigned int have_except:1; /* ban exemptions +e */
|
||||
unsigned int have_invite:1; /* invite exemptions +I */
|
||||
|
||||
@@ -341,6 +341,7 @@ cv_tabs_init (chanview *cv)
|
||||
GtkWidget *box;
|
||||
GtkWidget *viewport;
|
||||
GtkWidget *outer;
|
||||
GtkWidget *tree;
|
||||
|
||||
if (cv->vertical)
|
||||
{
|
||||
@@ -385,6 +386,11 @@ cv_tabs_init (chanview *cv)
|
||||
gtk_container_add (GTK_CONTAINER (viewport), box);
|
||||
gtk_widget_show (box);
|
||||
|
||||
tree = gtk_tree_view_new_with_model (GTK_TREE_MODEL (cv->store));
|
||||
gtk_widget_set_name (tree, "zoitechat-tree");
|
||||
gtk_widget_set_no_show_all (tree, TRUE);
|
||||
gtk_box_pack_start (GTK_BOX (outer), tree, 0, 0, 0);
|
||||
|
||||
gtk_container_add (GTK_CONTAINER (cv->box), outer);
|
||||
}
|
||||
|
||||
|
||||
@@ -79,6 +79,8 @@ struct _chanview
|
||||
guint theme_listener_id;
|
||||
};
|
||||
|
||||
static void chanview_set_font_desc (chanview *cv, const PangoFontDescription *font_desc);
|
||||
|
||||
struct _chan
|
||||
{
|
||||
chanview *cv; /* our owner */
|
||||
@@ -169,9 +171,10 @@ chanview_apply_theme (chanview *cv)
|
||||
w = GTK_WIDGET (tv->tree);
|
||||
if (input_style)
|
||||
font = input_style->font_desc;
|
||||
chanview_set_font_desc (cv, font);
|
||||
|
||||
theme_manager_apply_channel_tree_style (w,
|
||||
theme_manager_get_channel_tree_palette_behavior (font));
|
||||
theme_manager_get_channel_tree_palette_behavior (cv->font_desc));
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -313,6 +316,14 @@ chanview_free_ch (chanview *cv, GtkTreeIter *iter)
|
||||
g_free (ch);
|
||||
}
|
||||
|
||||
static void
|
||||
chanview_set_font_desc (chanview *cv, const PangoFontDescription *font_desc)
|
||||
{
|
||||
if (cv->font_desc)
|
||||
pango_font_description_free (cv->font_desc);
|
||||
cv->font_desc = font_desc ? pango_font_description_copy (font_desc) : NULL;
|
||||
}
|
||||
|
||||
static void
|
||||
chanview_destroy_store (chanview *cv) /* free every (chan *) in the store */
|
||||
{
|
||||
@@ -335,6 +346,9 @@ chanview_destroy (chanview *cv)
|
||||
if (cv->box)
|
||||
gtk_widget_destroy (cv->box);
|
||||
|
||||
if (cv->font_desc)
|
||||
pango_font_description_free (cv->font_desc);
|
||||
|
||||
chanview_destroy_store (cv);
|
||||
g_free (cv);
|
||||
}
|
||||
@@ -356,7 +370,7 @@ chanview_new (int type, int trunc_len, gboolean sort, gboolean use_icons,
|
||||
cv = g_new0 (chanview, 1);
|
||||
cv->store = gtk_tree_store_new (5, G_TYPE_STRING, G_TYPE_POINTER,
|
||||
PANGO_TYPE_ATTR_LIST, GDK_TYPE_PIXBUF, G_TYPE_INT);
|
||||
cv->font_desc = font_desc;
|
||||
cv->font_desc = font_desc ? pango_font_description_copy (font_desc) : NULL;
|
||||
cv->box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
cv->trunc_len = trunc_len;
|
||||
cv->sorted = sort;
|
||||
|
||||
@@ -812,18 +812,19 @@ fe_set_topic (session *sess, char *topic, char *stripped_topic)
|
||||
{
|
||||
if (!sess->gui->is_tab || sess == current_tab)
|
||||
{
|
||||
GtkTextBuffer *topic_buffer;
|
||||
GtkTextIter start;
|
||||
|
||||
topic_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (sess->gui->topic_entry));
|
||||
if (prefs.hex_text_stripcolor_topic)
|
||||
{
|
||||
gtk_text_buffer_set_text (
|
||||
gtk_text_view_get_buffer (GTK_TEXT_VIEW (sess->gui->topic_entry)),
|
||||
stripped_topic, -1);
|
||||
}
|
||||
gtk_text_buffer_set_text (topic_buffer, stripped_topic, -1);
|
||||
else
|
||||
{
|
||||
gtk_text_buffer_set_text (
|
||||
gtk_text_view_get_buffer (GTK_TEXT_VIEW (sess->gui->topic_entry)),
|
||||
topic, -1);
|
||||
}
|
||||
gtk_text_buffer_set_text (topic_buffer, topic, -1);
|
||||
|
||||
gtk_text_buffer_get_start_iter (topic_buffer, &start);
|
||||
gtk_text_buffer_place_cursor (topic_buffer, &start);
|
||||
gtk_text_view_scroll_to_iter (GTK_TEXT_VIEW (sess->gui->topic_entry),
|
||||
&start, 0.0, FALSE, 0.0, 0.0);
|
||||
mg_set_topic_tip (sess);
|
||||
}
|
||||
else
|
||||
@@ -1356,17 +1357,19 @@ fe_open_url_inner (const char *url)
|
||||
gboolean opened = FALSE;
|
||||
|
||||
#ifdef WIN32
|
||||
opened = g_app_info_launch_default_for_uri (escaped_url, NULL, &error);
|
||||
gunichar2 *url_utf16 = g_utf8_to_utf16 (escaped_url, -1, NULL, NULL, NULL);
|
||||
|
||||
if (url_utf16 != NULL)
|
||||
{
|
||||
opened = ((INT_PTR) ShellExecuteW (0, L"open", url_utf16, NULL, NULL, SW_SHOWNORMAL)) > 32;
|
||||
g_free (url_utf16);
|
||||
}
|
||||
|
||||
if (!opened)
|
||||
{
|
||||
g_clear_error (&error);
|
||||
gunichar2 *url_utf16 = g_utf8_to_utf16 (escaped_url, -1, NULL, NULL, NULL);
|
||||
|
||||
if (url_utf16 != NULL)
|
||||
{
|
||||
opened = ((INT_PTR) ShellExecuteW (0, L"open", url_utf16, NULL, NULL, SW_SHOWNORMAL)) > 32;
|
||||
g_free (url_utf16);
|
||||
}
|
||||
opened = g_app_info_launch_default_for_uri (escaped_url, NULL, &error);
|
||||
if (!opened)
|
||||
g_clear_error (&error);
|
||||
}
|
||||
#else
|
||||
{
|
||||
|
||||
@@ -163,6 +163,8 @@ typedef struct session_gui
|
||||
*op_xpm, /* icon to the left of nickname */
|
||||
*namelistinfo, /* label above userlist */
|
||||
*input_box,
|
||||
*reply_box,
|
||||
*reply_label,
|
||||
*flag_wid[NUM_FLAG_WIDS], /* channelmode buttons */
|
||||
*limit_entry, /* +l */
|
||||
*key_entry; /* +k */
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
#define ICON_FKEYS_DELETE "edit-delete"
|
||||
#define ICON_FKEYS_CANCEL "dialog-cancel"
|
||||
#define ICON_FKEYS_SAVE "document-save"
|
||||
#define ICON_FKEYS_RESET "edit-undo"
|
||||
|
||||
static void replace_handle (GtkWidget * wid);
|
||||
void key_check_replace_on_change (GtkEditable *editable, gpointer data);
|
||||
@@ -80,7 +81,7 @@ void key_action_tab_clean (void);
|
||||
*/
|
||||
|
||||
/* Remember that the *number* of actions is this *plus* 1 --AGL */
|
||||
#define KEY_MAX_ACTIONS 14
|
||||
#define KEY_MAX_ACTIONS 16
|
||||
|
||||
struct key_binding
|
||||
{
|
||||
@@ -105,7 +106,10 @@ struct gcomp_data
|
||||
};
|
||||
|
||||
static int key_load_kbs (void);
|
||||
static int key_load_kbs_from_buffer (char *ibuf, off_t size, GSList **out_list);
|
||||
static int key_save_kbs (void);
|
||||
static void key_dialog_load (GtkListStore *store);
|
||||
static void key_dialog_reset (GtkWidget *wid, gpointer userdata);
|
||||
static int key_action_handle_command (GtkWidget * wid, GdkEventKey * evt,
|
||||
char *d1, char *d2,
|
||||
struct session *sess);
|
||||
@@ -142,6 +146,13 @@ static int key_action_move_tab_family_right (GtkWidget * wid, GdkEventKey * evt,
|
||||
static int key_action_put_history (GtkWidget * wid, GdkEventKey * evt,
|
||||
char *d1, char *d2,
|
||||
struct session *sess);
|
||||
static int key_action_menu_shortcut (GtkWidget * wid, GdkEventKey * evt,
|
||||
char *d1, char *d2,
|
||||
struct session *sess);
|
||||
static int key_action_reopen_closed_tab (GtkWidget * wid, GdkEventKey * evt,
|
||||
char *d1, char *d2,
|
||||
struct session *sess);
|
||||
|
||||
|
||||
static GSList *keybind_list = NULL;
|
||||
|
||||
@@ -260,7 +271,7 @@ static const struct key_action key_actions[KEY_MAX_ACTIONS + 1] = {
|
||||
{key_action_handle_command, "Run Command",
|
||||
N_("The \002Run Command\002 action runs the data in Data 1 as if it had been typed into the entry box where you pressed the key sequence. Thus it can contain text (which will be sent to the channel/person), commands or user commands. Escapes in Data 1 are interpreted (for example \002\\n\002, \002\\r\002, \002\\t\002, \002\\xNN\002 and \002\\\\\002), so it is possible to run more than one command by using newlines.")},
|
||||
{key_action_page_switch, "Change Page",
|
||||
N_("The \002Change Page\002 command switches between pages in the notebook. Set Data 1 to the page you want to switch to. If Data 2 is set to anything then the switch will be relative to the current position. Set Data 1 to auto to switch to the page with the most recent and important activity (queries first, then channels with hilight, channels with dialogue, channels with other data)")},
|
||||
N_("The \002Change Page\002 command switches between pages in the notebook. Set Data 1 to the page number, channel name, network/channel, or auto. If Data 2 is set to anything then numeric switches are relative to the current position. Auto switches to the page with the most recent and important activity (queries first, then channels with hilight, channels with dialogue, channels with other data)")},
|
||||
{key_action_insert, "Insert in Buffer",
|
||||
N_("The \002Insert in Buffer\002 command will insert the contents of Data 1 into the entry where the key sequence was pressed at the current cursor position. Escapes in Data 1 are interpreted (for example \002\\n\002, \002\\r\002, \002\\t\002, \002\\xNN\002 and \002\\\\\002).")},
|
||||
{key_action_scroll_page, "Scroll Page",
|
||||
@@ -287,6 +298,10 @@ static const struct key_action key_actions[KEY_MAX_ACTIONS + 1] = {
|
||||
N_("This command moves the current tab family to the right")},
|
||||
{key_action_put_history, "Push input line into history",
|
||||
N_("Push input line into history but doesn't send to server")},
|
||||
{key_action_menu_shortcut, "Menu Shortcut",
|
||||
N_("Runs one of the built-in menu shortcuts. Set Data 1 to: network-list, new-server-tab, new-server-window, close, quit, menu-toggle, user-list-toggle, fullscreen-toggle, away-toggle, reset-marker, move-marker, copy-selection, search-text, search-next, search-previous or contents.")},
|
||||
{key_action_reopen_closed_tab, "Reopen Closed Tab",
|
||||
N_("Reopens the most recently closed channel tab")},
|
||||
};
|
||||
|
||||
#define default_kb_cfg \
|
||||
@@ -329,7 +344,86 @@ static const struct key_action key_actions[KEY_MAX_ACTIONS + 1] = {
|
||||
"ACCEL=<Alt>Right\nMove front tab right\nD1!\nD2!\n\n"\
|
||||
"ACCEL=<Primary><Shift>Page_Up\nMove tab family left\nD1!\nD2!\n\n"\
|
||||
"ACCEL=<Primary><Shift>Page_Down\nMove tab family right\nD1!\nD2!\n\n"\
|
||||
"ACCEL=F9\nRun Command\nD1:/GUI MENU TOGGLE\nD2!\n\n"
|
||||
"ACCEL=<Primary>s\nMenu Shortcut\nD1:network-list\nD2!\n\n"\
|
||||
"ACCEL=<Primary>t\nMenu Shortcut\nD1:new-server-tab\nD2!\n\n"\
|
||||
"ACCEL=<Primary>n\nMenu Shortcut\nD1:new-server-window\nD2!\n\n"\
|
||||
"ACCEL=<Primary>w\nMenu Shortcut\nD1:close\nD2!\n\n"\
|
||||
"ACCEL=<Primary>q\nMenu Shortcut\nD1:quit\nD2!\n\n"\
|
||||
"ACCEL=<Primary>F9\nMenu Shortcut\nD1:menu-toggle\nD2!\n\n"\
|
||||
"ACCEL=F7\nMenu Shortcut\nD1:user-list-toggle\nD2!\n\n"\
|
||||
"ACCEL=F11\nMenu Shortcut\nD1:fullscreen-toggle\nD2!\n\n"\
|
||||
"ACCEL=<Alt>a\nMenu Shortcut\nD1:away-toggle\nD2!\n\n"\
|
||||
"ACCEL=<Primary>m\nMenu Shortcut\nD1:reset-marker\nD2!\n\n"\
|
||||
"ACCEL=<Primary><Shift>M\nMenu Shortcut\nD1:move-marker\nD2!\n\n"\
|
||||
"ACCEL=<Primary><Shift>C\nMenu Shortcut\nD1:copy-selection\nD2!\n\n"\
|
||||
"ACCEL=<Primary>f\nMenu Shortcut\nD1:search-text\nD2!\n\n"\
|
||||
"ACCEL=<Primary>g\nMenu Shortcut\nD1:search-next\nD2!\n\n"\
|
||||
"ACCEL=<Primary><Shift>G\nMenu Shortcut\nD1:search-previous\nD2!\n\n"\
|
||||
"ACCEL=F1\nMenu Shortcut\nD1:contents\nD2!\n\n"\
|
||||
"ACCEL=<Primary><Shift>T\nReopen Closed Tab\nD1!\nD2!\n\n"
|
||||
|
||||
static gboolean
|
||||
key_builtin_data_match (char *line, char *data)
|
||||
{
|
||||
if (line[2] == '!')
|
||||
return data == NULL || data[0] == 0;
|
||||
|
||||
if (line[2] == ':')
|
||||
return !strcmp (&line[3], data ? data : "");
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
key_binding_is_builtin (struct key_binding *kb)
|
||||
{
|
||||
char *buf, *ibuf;
|
||||
char *action;
|
||||
int pnt = 0;
|
||||
int state = 0;
|
||||
gboolean match = FALSE;
|
||||
gboolean d1_match = FALSE;
|
||||
off_t size;
|
||||
|
||||
if (kb->action < 0 || kb->action > KEY_MAX_ACTIONS)
|
||||
return FALSE;
|
||||
|
||||
action = key_actions[kb->action].name;
|
||||
ibuf = g_strdup (default_kb_cfg);
|
||||
size = strlen (default_kb_cfg);
|
||||
|
||||
while (buf_get_line (ibuf, &buf, &pnt, size))
|
||||
{
|
||||
if (strlen (buf) == 0)
|
||||
continue;
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case 0:
|
||||
state = 1;
|
||||
break;
|
||||
case 1:
|
||||
match = !strcmp (buf, action);
|
||||
state = 2;
|
||||
break;
|
||||
case 2:
|
||||
d1_match = match && key_builtin_data_match (buf, kb->data1);
|
||||
state = 3;
|
||||
break;
|
||||
case 3:
|
||||
if (d1_match && key_builtin_data_match (buf, kb->data2))
|
||||
{
|
||||
g_free (ibuf);
|
||||
return TRUE;
|
||||
}
|
||||
state = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
g_free (ibuf);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
void
|
||||
key_init ()
|
||||
@@ -443,28 +537,6 @@ key_handle_key_press (GtkWidget *wid, GdkEventKey *evt, session *sess)
|
||||
if (!list)
|
||||
return FALSE;
|
||||
current_sess = sess;
|
||||
if ((evt->state & GDK_CONTROL_MASK) &&
|
||||
!(evt->state & (GDK_MOD1_MASK | GDK_META_MASK)))
|
||||
{
|
||||
if (!(evt->state & GDK_SHIFT_MASK) &&
|
||||
(evt->keyval == GDK_KEY_w || evt->keyval == GDK_KEY_W))
|
||||
{
|
||||
if (sess->type == SESS_CHANNEL)
|
||||
{
|
||||
fe_close_window (sess);
|
||||
g_signal_stop_emission_by_name (G_OBJECT (wid), "key-press-event");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if ((evt->state & GDK_SHIFT_MASK) &&
|
||||
(evt->keyval == GDK_KEY_t || evt->keyval == GDK_KEY_T))
|
||||
{
|
||||
mg_reopen_closed_channel_tab ();
|
||||
g_signal_stop_emission_by_name (G_OBJECT (wid), "key-press-event");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (plugin_emit_keypress (sess, evt->state, evt->keyval, gdk_keyval_to_unicode (evt->keyval)))
|
||||
return 1;
|
||||
|
||||
@@ -508,6 +580,31 @@ key_handle_key_press (GtkWidget *wid, GdkEventKey *evt, session *sess)
|
||||
return 0;
|
||||
}
|
||||
|
||||
gboolean
|
||||
key_get_menu_accel (const char *name, guint *keyval, GdkModifierType *mod)
|
||||
{
|
||||
struct key_binding *kb;
|
||||
GSList *list;
|
||||
|
||||
if (!name)
|
||||
return FALSE;
|
||||
|
||||
list = keybind_list;
|
||||
while (list)
|
||||
{
|
||||
kb = (struct key_binding*)list->data;
|
||||
if (kb->action >= 0 && kb->action <= KEY_MAX_ACTIONS && kb->keyval != 0 && !strcmp (key_actions[kb->action].name, "Menu Shortcut") && kb->data1 && !strcmp (kb->data1, name))
|
||||
{
|
||||
*keyval = kb->keyval;
|
||||
*mod = kb->mod;
|
||||
return TRUE;
|
||||
}
|
||||
list = g_slist_next (list);
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
|
||||
/* ***** GUI code here ******************* */
|
||||
|
||||
@@ -518,6 +615,7 @@ enum
|
||||
ACTION_COLUMN,
|
||||
D1_COLUMN,
|
||||
D2_COLUMN,
|
||||
CUSTOM_COLUMN,
|
||||
N_COLUMNS
|
||||
};
|
||||
|
||||
@@ -642,16 +740,32 @@ key_dialog_keypress (GtkWidget *wid, GdkEventKey *evt, gpointer userdata)
|
||||
|
||||
if (handled)
|
||||
{
|
||||
gboolean custom1, custom2;
|
||||
|
||||
sel = gtk_tree_view_get_selection (view);
|
||||
gtk_tree_selection_get_selected (sel, &store, &iter1);
|
||||
if (!gtk_tree_selection_get_selected (sel, &store, &iter1))
|
||||
return FALSE;
|
||||
|
||||
path = gtk_tree_model_get_path (store, &iter1);
|
||||
if (delta == 1)
|
||||
gtk_tree_path_next (path);
|
||||
else
|
||||
gtk_tree_path_prev (path);
|
||||
gtk_tree_model_get_iter (store, &iter2, path);
|
||||
else if (!gtk_tree_path_prev (path))
|
||||
{
|
||||
gtk_tree_path_free (path);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (!gtk_tree_model_get_iter (store, &iter2, path))
|
||||
{
|
||||
gtk_tree_path_free (path);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
gtk_tree_path_free (path);
|
||||
gtk_list_store_swap (GTK_LIST_STORE (store), &iter1, &iter2);
|
||||
gtk_tree_model_get (store, &iter1, CUSTOM_COLUMN, &custom1, -1);
|
||||
gtk_tree_model_get (store, &iter2, CUSTOM_COLUMN, &custom2, -1);
|
||||
if (custom1 && custom2)
|
||||
gtk_list_store_swap (GTK_LIST_STORE (store), &iter1, &iter2);
|
||||
}
|
||||
|
||||
return handled;
|
||||
@@ -663,14 +777,23 @@ key_dialog_selection_changed (GtkTreeSelection *sel, gpointer userdata)
|
||||
GtkTreeModel *model;
|
||||
GtkTreeIter iter;
|
||||
GtkXText *xtext;
|
||||
GtkWidget *delete_button;
|
||||
char *actiontext;
|
||||
gboolean custom;
|
||||
int action;
|
||||
|
||||
delete_button = g_object_get_data (G_OBJECT (key_dialog), "delete_button");
|
||||
if (!gtk_tree_selection_get_selected (sel, &model, &iter) || model == NULL)
|
||||
{
|
||||
if (delete_button)
|
||||
gtk_widget_set_sensitive (delete_button, FALSE);
|
||||
return;
|
||||
}
|
||||
|
||||
xtext = GTK_XTEXT (g_object_get_data (G_OBJECT (key_dialog), "xtext"));
|
||||
gtk_tree_model_get (model, &iter, ACTION_COLUMN, &actiontext, -1);
|
||||
gtk_tree_model_get (model, &iter, ACTION_COLUMN, &actiontext, CUSTOM_COLUMN, &custom, -1);
|
||||
if (delete_button)
|
||||
gtk_widget_set_sensitive (delete_button, custom);
|
||||
|
||||
if (actiontext)
|
||||
{
|
||||
@@ -745,7 +868,10 @@ key_dialog_save (GtkWidget *wid, gpointer userdata)
|
||||
}
|
||||
|
||||
if (key_save_kbs () == 0)
|
||||
{
|
||||
menu_update_quit_accel ();
|
||||
key_dialog_close (wid, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -758,6 +884,7 @@ key_dialog_add (GtkWidget *wid, gpointer userdata)
|
||||
GtkTreePath *path;
|
||||
|
||||
gtk_list_store_append (store, &iter);
|
||||
gtk_list_store_set (store, &iter, CUSTOM_COLUMN, TRUE, -1);
|
||||
|
||||
/* make sure the new row is visible and selected */
|
||||
path = gtk_tree_model_get_path (GTK_TREE_MODEL (store), &iter);
|
||||
@@ -767,6 +894,134 @@ key_dialog_add (GtkWidget *wid, gpointer userdata)
|
||||
gtk_tree_path_free (path);
|
||||
}
|
||||
|
||||
static char *
|
||||
key_binding_signature (const char *action, const char *data1, const char *data2)
|
||||
{
|
||||
return g_strdup_printf ("%s\\n%s\\n%s", action ? action : "", data1 ? data1 : "", data2 ? data2 : "");
|
||||
}
|
||||
|
||||
static int
|
||||
key_dialog_reset_count (GHashTable *table, const char *key)
|
||||
{
|
||||
return GPOINTER_TO_INT (g_hash_table_lookup (table, key));
|
||||
}
|
||||
|
||||
static void
|
||||
key_dialog_reset_increment (GHashTable *table, char *key)
|
||||
{
|
||||
g_hash_table_replace (table, key, GINT_TO_POINTER (key_dialog_reset_count (table, key) + 1));
|
||||
}
|
||||
|
||||
static void
|
||||
key_dialog_reset (GtkWidget *wid, gpointer userdata)
|
||||
{
|
||||
GtkListStore *store = GTK_LIST_STORE (get_store ());
|
||||
GtkListStore *custom_store;
|
||||
GtkTreeIter iter, custom_iter;
|
||||
GtkWidget *delete_button;
|
||||
GHashTable *default_counts, *seen_counts;
|
||||
GSList *list = NULL, *old_list, *default_iter;
|
||||
struct key_binding *kb;
|
||||
gboolean custom, keep;
|
||||
char *key, *accel, *action, *data1, *data2, *signature;
|
||||
|
||||
if (key_load_kbs_from_buffer (g_strdup (default_kb_cfg), strlen (default_kb_cfg), &list) != 0)
|
||||
return;
|
||||
|
||||
default_counts = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
|
||||
seen_counts = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
|
||||
for (default_iter = list; default_iter; default_iter = g_slist_next (default_iter))
|
||||
{
|
||||
kb = default_iter->data;
|
||||
signature = key_binding_signature (key_actions[kb->action].name, kb->data1, kb->data2);
|
||||
key_dialog_reset_increment (default_counts, signature);
|
||||
}
|
||||
|
||||
custom_store = gtk_list_store_new (N_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING,
|
||||
G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN);
|
||||
|
||||
if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (store), &iter))
|
||||
{
|
||||
do
|
||||
{
|
||||
gtk_tree_model_get (GTK_TREE_MODEL (store), &iter,
|
||||
KEY_COLUMN, &key,
|
||||
ACCEL_COLUMN, &accel,
|
||||
ACTION_COLUMN, &action,
|
||||
D1_COLUMN, &data1,
|
||||
D2_COLUMN, &data2,
|
||||
CUSTOM_COLUMN, &custom,
|
||||
-1);
|
||||
signature = key_binding_signature (action, data1, data2);
|
||||
keep = custom || key_dialog_reset_count (seen_counts, signature) >= key_dialog_reset_count (default_counts, signature);
|
||||
if (!custom)
|
||||
key_dialog_reset_increment (seen_counts, g_strdup (signature));
|
||||
if (keep)
|
||||
{
|
||||
gtk_list_store_append (custom_store, &custom_iter);
|
||||
gtk_list_store_set (custom_store, &custom_iter,
|
||||
KEY_COLUMN, key,
|
||||
ACCEL_COLUMN, accel,
|
||||
ACTION_COLUMN, action,
|
||||
D1_COLUMN, data1,
|
||||
D2_COLUMN, data2,
|
||||
CUSTOM_COLUMN, TRUE,
|
||||
-1);
|
||||
}
|
||||
g_free (signature);
|
||||
g_free (key);
|
||||
g_free (accel);
|
||||
g_free (action);
|
||||
g_free (data1);
|
||||
g_free (data2);
|
||||
}
|
||||
while (gtk_tree_model_iter_next (GTK_TREE_MODEL (store), &iter));
|
||||
}
|
||||
|
||||
old_list = keybind_list;
|
||||
keybind_list = list;
|
||||
gtk_list_store_clear (store);
|
||||
key_dialog_load (store);
|
||||
keybind_list = old_list;
|
||||
|
||||
if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (custom_store), &iter))
|
||||
{
|
||||
do
|
||||
{
|
||||
gtk_tree_model_get (GTK_TREE_MODEL (custom_store), &iter,
|
||||
KEY_COLUMN, &key,
|
||||
ACCEL_COLUMN, &accel,
|
||||
ACTION_COLUMN, &action,
|
||||
D1_COLUMN, &data1,
|
||||
D2_COLUMN, &data2,
|
||||
-1);
|
||||
gtk_list_store_append (store, &custom_iter);
|
||||
gtk_list_store_set (store, &custom_iter,
|
||||
KEY_COLUMN, key,
|
||||
ACCEL_COLUMN, accel,
|
||||
ACTION_COLUMN, action,
|
||||
D1_COLUMN, data1,
|
||||
D2_COLUMN, data2,
|
||||
CUSTOM_COLUMN, TRUE,
|
||||
-1);
|
||||
g_free (key);
|
||||
g_free (accel);
|
||||
g_free (action);
|
||||
g_free (data1);
|
||||
g_free (data2);
|
||||
}
|
||||
while (gtk_tree_model_iter_next (GTK_TREE_MODEL (custom_store), &iter));
|
||||
}
|
||||
|
||||
delete_button = g_object_get_data (G_OBJECT (key_dialog), "delete_button");
|
||||
if (delete_button)
|
||||
gtk_widget_set_sensitive (delete_button, FALSE);
|
||||
g_hash_table_destroy (default_counts);
|
||||
g_hash_table_destroy (seen_counts);
|
||||
g_object_unref (custom_store);
|
||||
g_slist_free_full (list, key_free);
|
||||
}
|
||||
|
||||
static void
|
||||
key_dialog_delete (GtkWidget *wid, gpointer userdata)
|
||||
{
|
||||
@@ -774,9 +1029,14 @@ key_dialog_delete (GtkWidget *wid, gpointer userdata)
|
||||
GtkListStore *store = GTK_LIST_STORE (gtk_tree_view_get_model (view));
|
||||
GtkTreeIter iter;
|
||||
GtkTreePath *path;
|
||||
gboolean custom;
|
||||
|
||||
if (gtkutil_treeview_get_selected (view, &iter, -1))
|
||||
{
|
||||
gtk_tree_model_get (GTK_TREE_MODEL (store), &iter, CUSTOM_COLUMN, &custom, -1);
|
||||
if (!custom)
|
||||
return;
|
||||
|
||||
/* delete this row, select next one */
|
||||
if (gtk_list_store_remove (store, &iter))
|
||||
{
|
||||
@@ -803,13 +1063,13 @@ key_dialog_treeview_new (GtkWidget *box)
|
||||
gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scroll), GTK_SHADOW_IN);
|
||||
|
||||
store = gtk_list_store_new (N_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING,
|
||||
G_TYPE_STRING, G_TYPE_STRING);
|
||||
G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN);
|
||||
g_return_val_if_fail (store != NULL, NULL);
|
||||
|
||||
view = gtk_tree_view_new_with_model (GTK_TREE_MODEL (store));
|
||||
gtk_tree_view_set_fixed_height_mode (GTK_TREE_VIEW (view), TRUE);
|
||||
gtk_tree_view_set_enable_search (GTK_TREE_VIEW (view), FALSE);
|
||||
gtk_tree_view_set_reorderable (GTK_TREE_VIEW (view), TRUE);
|
||||
gtk_tree_view_set_reorderable (GTK_TREE_VIEW (view), FALSE);
|
||||
|
||||
g_signal_connect (G_OBJECT (view), "key-press-event",
|
||||
G_CALLBACK (key_dialog_keypress), NULL);
|
||||
@@ -879,7 +1139,8 @@ key_dialog_treeview_new (GtkWidget *box)
|
||||
G_CALLBACK (key_dialog_combo_changed), combostore);
|
||||
gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (view), ACTION_COLUMN,
|
||||
"Action", render,
|
||||
"text", ACTION_COLUMN,
|
||||
"text", ACTION_COLUMN,
|
||||
"editable", CUSTOM_COLUMN,
|
||||
NULL);
|
||||
|
||||
render = gtk_cell_renderer_text_new ();
|
||||
@@ -890,6 +1151,7 @@ key_dialog_treeview_new (GtkWidget *box)
|
||||
GTK_TREE_VIEW (view), D1_COLUMN,
|
||||
"Data1", render,
|
||||
"text", D1_COLUMN,
|
||||
"editable", CUSTOM_COLUMN,
|
||||
NULL);
|
||||
|
||||
render = gtk_cell_renderer_text_new ();
|
||||
@@ -900,6 +1162,7 @@ key_dialog_treeview_new (GtkWidget *box)
|
||||
GTK_TREE_VIEW (view), D2_COLUMN,
|
||||
"Data2", render,
|
||||
"text", D2_COLUMN,
|
||||
"editable", CUSTOM_COLUMN,
|
||||
NULL);
|
||||
|
||||
col = gtk_tree_view_get_column (GTK_TREE_VIEW (view), KEY_COLUMN);
|
||||
@@ -945,7 +1208,8 @@ key_dialog_load (GtkListStore *store)
|
||||
ACCEL_COLUMN, accel_text,
|
||||
ACTION_COLUMN, key_actions[kb->action].name,
|
||||
D1_COLUMN, kb->data1,
|
||||
D2_COLUMN, kb->data2, -1);
|
||||
D2_COLUMN, kb->data2,
|
||||
CUSTOM_COLUMN, !key_binding_is_builtin (kb), -1);
|
||||
|
||||
g_free (accel_text);
|
||||
g_free (label_text);
|
||||
@@ -958,7 +1222,7 @@ void
|
||||
key_dialog_show ()
|
||||
{
|
||||
GtkWidget *vbox, *box;
|
||||
GtkWidget *view, *xtext;
|
||||
GtkWidget *view, *xtext, *delete_button;
|
||||
GtkListStore *store;
|
||||
XTextColor xtext_palette[XTEXT_COLS];
|
||||
char buf[128];
|
||||
@@ -992,8 +1256,12 @@ key_dialog_show ()
|
||||
|
||||
gtkutil_button (box, ICON_FKEYS_NEW, NULL, key_dialog_add,
|
||||
NULL, _("Add"));
|
||||
gtkutil_button (box, ICON_FKEYS_DELETE, NULL, key_dialog_delete,
|
||||
delete_button = gtkutil_button (box, ICON_FKEYS_DELETE, NULL, key_dialog_delete,
|
||||
NULL, _("Delete"));
|
||||
g_object_set_data (G_OBJECT (key_dialog), "delete_button", delete_button);
|
||||
gtk_widget_set_sensitive (delete_button, FALSE);
|
||||
gtkutil_button (box, ICON_FKEYS_RESET, NULL, key_dialog_reset,
|
||||
NULL, _("Reset"));
|
||||
gtkutil_button (box, ICON_FKEYS_CANCEL, NULL, key_dialog_close,
|
||||
NULL, _("Cancel"));
|
||||
gtkutil_button (box, ICON_FKEYS_SAVE, NULL, key_dialog_save,
|
||||
@@ -1101,41 +1369,14 @@ key_load_kbs_helper_mod (char *buf, GdkModifierType *out)
|
||||
}
|
||||
|
||||
static int
|
||||
key_load_kbs (void)
|
||||
key_load_kbs_from_buffer (char *ibuf, off_t size, GSList **out_list)
|
||||
{
|
||||
char *buf, *ibuf;
|
||||
struct stat st;
|
||||
char *buf;
|
||||
struct key_binding *kb = NULL;
|
||||
int fd, len, state = 0, pnt = 0;
|
||||
int len, state = 0, pnt = 0;
|
||||
guint keyval;
|
||||
GdkModifierType mod = 0;
|
||||
off_t size;
|
||||
|
||||
fd = zoitechat_open_file ("keybindings.conf", O_RDONLY, 0, 0);
|
||||
if (fd < 0)
|
||||
{
|
||||
ibuf = g_strdup (default_kb_cfg);
|
||||
size = strlen (default_kb_cfg);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (fstat (fd, &st) != 0)
|
||||
{
|
||||
close (fd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
ibuf = g_malloc(st.st_size);
|
||||
read (fd, ibuf, st.st_size);
|
||||
size = st.st_size;
|
||||
close (fd);
|
||||
}
|
||||
|
||||
if (keybind_list)
|
||||
{
|
||||
g_slist_free_full (keybind_list, key_free);
|
||||
keybind_list = NULL;
|
||||
}
|
||||
GSList *list = NULL;
|
||||
|
||||
while (buf_get_line (ibuf, &buf, &pnt, size))
|
||||
{
|
||||
@@ -1149,14 +1390,12 @@ key_load_kbs (void)
|
||||
case KBSTATE_MOD:
|
||||
kb = g_new0 (struct key_binding, 1);
|
||||
|
||||
/* New format */
|
||||
if (strncmp (buf, "ACCEL=", 6) == 0)
|
||||
{
|
||||
buf += 6;
|
||||
|
||||
gtk_accelerator_parse (buf, &keyval, &mod);
|
||||
|
||||
|
||||
kb->keyval = keyval;
|
||||
kb->mod = key_modifier_get_valid (mod);
|
||||
|
||||
@@ -1179,6 +1418,8 @@ key_load_kbs (void)
|
||||
if (keyval == 0)
|
||||
{
|
||||
g_free (ibuf);
|
||||
key_free (kb);
|
||||
g_slist_free_full (list, key_free);
|
||||
return 2;
|
||||
}
|
||||
|
||||
@@ -1195,6 +1436,8 @@ key_load_kbs (void)
|
||||
if (kb->action == KEY_MAX_ACTIONS + 1)
|
||||
{
|
||||
g_free (ibuf);
|
||||
key_free (kb);
|
||||
g_slist_free_full (list, key_free);
|
||||
return 3;
|
||||
}
|
||||
|
||||
@@ -1212,6 +1455,8 @@ key_load_kbs (void)
|
||||
if (buf[0] != 'D')
|
||||
{
|
||||
g_free (ibuf);
|
||||
key_free (kb);
|
||||
g_slist_free_full (list, key_free);
|
||||
return 4;
|
||||
}
|
||||
|
||||
@@ -1232,7 +1477,6 @@ key_load_kbs (void)
|
||||
if (buf[2] == ':')
|
||||
{
|
||||
len = strlen (buf);
|
||||
/* Add one for the NULL, subtract 3 for the "Dx:" */
|
||||
len++;
|
||||
len -= 3;
|
||||
if (state == KBSTATE_DT1)
|
||||
@@ -1255,7 +1499,8 @@ key_load_kbs (void)
|
||||
continue;
|
||||
} else
|
||||
{
|
||||
keybind_list = g_slist_append (keybind_list, kb);
|
||||
list = g_slist_append (list, kb);
|
||||
kb = NULL;
|
||||
|
||||
state = KBSTATE_MOD;
|
||||
}
|
||||
@@ -1264,14 +1509,56 @@ key_load_kbs (void)
|
||||
}
|
||||
}
|
||||
g_free (ibuf);
|
||||
*out_list = list;
|
||||
return 0;
|
||||
|
||||
corrupt_file:
|
||||
g_free (ibuf);
|
||||
g_free (kb);
|
||||
key_free (kb);
|
||||
g_slist_free_full (list, key_free);
|
||||
return 5;
|
||||
}
|
||||
|
||||
static int
|
||||
key_load_kbs (void)
|
||||
{
|
||||
char *ibuf;
|
||||
struct stat st;
|
||||
int fd, result;
|
||||
off_t size;
|
||||
GSList *list = NULL;
|
||||
|
||||
fd = zoitechat_open_file ("keybindings.conf", O_RDONLY, 0, 0);
|
||||
if (fd < 0)
|
||||
{
|
||||
ibuf = g_strdup (default_kb_cfg);
|
||||
size = strlen (default_kb_cfg);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (fstat (fd, &st) != 0)
|
||||
{
|
||||
close (fd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
ibuf = g_malloc(st.st_size);
|
||||
read (fd, ibuf, st.st_size);
|
||||
size = st.st_size;
|
||||
close (fd);
|
||||
}
|
||||
|
||||
result = key_load_kbs_from_buffer (ibuf, size, &list);
|
||||
if (result != 0)
|
||||
return result;
|
||||
|
||||
if (keybind_list)
|
||||
g_slist_free_full (keybind_list, key_free);
|
||||
keybind_list = list;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
key_action_handle_command (GtkWidget * wid, GdkEventKey * evt, char *d1,
|
||||
char *d2, struct session *sess)
|
||||
@@ -1289,6 +1576,24 @@ key_action_handle_command (GtkWidget * wid, GdkEventKey * evt, char *d1,
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
key_action_menu_shortcut (GtkWidget * wid, GdkEventKey * evt, char *d1,
|
||||
char *d2, struct session *sess)
|
||||
{
|
||||
if (menu_key_action (d1, evt->keyval, evt->state))
|
||||
return 2;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
key_action_reopen_closed_tab (GtkWidget * wid, GdkEventKey * evt, char *d1,
|
||||
char *d2, struct session *sess)
|
||||
{
|
||||
mg_reopen_closed_channel_tab ();
|
||||
return 2;
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if the given session is inside the main window. This predicate
|
||||
* is passed to lastact_getfirst() as a way to filter out detached sessions.
|
||||
@@ -1307,6 +1612,9 @@ key_action_page_switch (GtkWidget * wid, GdkEventKey * evt, char *d1,
|
||||
char *d2, struct session *sess)
|
||||
{
|
||||
session *newsess;
|
||||
char *network;
|
||||
char *channel;
|
||||
char *slash;
|
||||
int len, i, num;
|
||||
|
||||
if (!d1)
|
||||
@@ -1318,21 +1626,13 @@ key_action_page_switch (GtkWidget * wid, GdkEventKey * evt, char *d1,
|
||||
|
||||
if (g_ascii_strcasecmp(d1, "auto") == 0)
|
||||
{
|
||||
/* Auto switch makes no sense in detached sessions */
|
||||
if (!sess->gui->is_tab)
|
||||
return 1;
|
||||
|
||||
/* Obtain a session with recent activity */
|
||||
newsess = lastact_getfirst(session_check_is_tab);
|
||||
|
||||
if (newsess)
|
||||
{
|
||||
/*
|
||||
* Only sessions in the current window should be considered (i.e.
|
||||
* we don't want to move the focus on a different window). This
|
||||
* call could, in theory, do this, but we checked before that
|
||||
* newsess->gui->is_tab and sess->gui->is_tab.
|
||||
*/
|
||||
mg_bring_tofront_sess(newsess);
|
||||
return 0;
|
||||
}
|
||||
@@ -1346,8 +1646,27 @@ key_action_page_switch (GtkWidget * wid, GdkEventKey * evt, char *d1,
|
||||
{
|
||||
if (i == 0 && (d1[i] == '+' || d1[i] == '-'))
|
||||
continue;
|
||||
else
|
||||
return 1;
|
||||
|
||||
network = NULL;
|
||||
channel = d1;
|
||||
slash = strchr (d1, '/');
|
||||
|
||||
if (slash && slash[1])
|
||||
{
|
||||
network = g_strndup (d1, slash - d1);
|
||||
channel = slash + 1;
|
||||
}
|
||||
|
||||
newsess = plugin_find_context (network, channel, sess->server);
|
||||
g_free (network);
|
||||
|
||||
if (newsess && newsess->gui)
|
||||
{
|
||||
mg_bring_tofront_sess(newsess);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,5 +35,6 @@ int key_handle_key_press (GtkWidget * wid, GdkEventKey * evt, session *sess);
|
||||
int key_action_insert (GtkWidget * wid, GdkEventKey * evt, char *d1, char *d2,
|
||||
session *sess);
|
||||
void key_check_replace_on_change (GtkEditable *editable, gpointer data);
|
||||
gboolean key_get_menu_accel (const char *name, guint *keyval, GdkModifierType *mod);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -527,6 +527,43 @@ gtkutil_destroy (GtkWidget * igad, GtkWidget * dgad)
|
||||
gtk_widget_destroy (dgad);
|
||||
}
|
||||
|
||||
static void
|
||||
gtkutil_place_dialog (GtkWidget *dialog)
|
||||
{
|
||||
GList *list;
|
||||
GList *item;
|
||||
GtkWindow *anchor = NULL;
|
||||
gint x;
|
||||
gint y;
|
||||
extern GtkWidget *parent_window;
|
||||
|
||||
list = gtk_window_list_toplevels ();
|
||||
for (item = list; item; item = item->next)
|
||||
{
|
||||
if (item->data != dialog && GTK_IS_WINDOW (item->data) &&
|
||||
gtk_widget_get_visible (GTK_WIDGET (item->data)) &&
|
||||
gtk_window_is_active (GTK_WINDOW (item->data)))
|
||||
{
|
||||
anchor = GTK_WINDOW (item->data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
g_list_free (list);
|
||||
|
||||
if (!anchor && parent_window && parent_window != dialog && GTK_IS_WINDOW (parent_window))
|
||||
anchor = GTK_WINDOW (parent_window);
|
||||
|
||||
if (anchor)
|
||||
{
|
||||
gtk_window_get_position (anchor, &x, &y);
|
||||
gtk_window_move (GTK_WINDOW (dialog), x, y);
|
||||
}
|
||||
else
|
||||
{
|
||||
gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_MOUSE);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
gtkutil_get_str_response (GtkDialog *dialog, gint arg1, gpointer entry)
|
||||
{
|
||||
@@ -540,15 +577,15 @@ gtkutil_get_str_response (GtkDialog *dialog, gint arg1, gpointer entry)
|
||||
|
||||
switch (arg1)
|
||||
{
|
||||
case GTK_RESPONSE_REJECT:
|
||||
callback (TRUE, text, user_data);
|
||||
gtk_widget_destroy (GTK_WIDGET (dialog));
|
||||
break;
|
||||
case GTK_RESPONSE_ACCEPT:
|
||||
callback (FALSE, text, user_data);
|
||||
gtk_widget_destroy (GTK_WIDGET (dialog));
|
||||
break;
|
||||
default:
|
||||
callback (TRUE, text, user_data);
|
||||
break;
|
||||
}
|
||||
|
||||
gtk_widget_destroy (GTK_WIDGET (dialog));
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -557,8 +594,8 @@ gtkutil_str_enter (GtkWidget *entry, GtkWidget *dialog)
|
||||
gtk_dialog_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT);
|
||||
}
|
||||
|
||||
void
|
||||
fe_get_str (char *msg, char *def, void *callback, void *userdata)
|
||||
static void
|
||||
gtkutil_get_str (char *msg, char *def, void *callback, void *userdata, gboolean visible)
|
||||
{
|
||||
GtkWidget *dialog;
|
||||
GtkWidget *entry;
|
||||
@@ -572,18 +609,11 @@ fe_get_str (char *msg, char *def, void *callback, void *userdata)
|
||||
NULL);
|
||||
theme_manager_attach_window (dialog);
|
||||
|
||||
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
|
||||
if (parent_window && GTK_IS_WINDOW (parent_window))
|
||||
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
|
||||
gtkutil_place_dialog (dialog);
|
||||
gtk_box_set_homogeneous (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), TRUE);
|
||||
|
||||
if (userdata == (void *)1) /* nick box is usually on the very bottom, make it centered */
|
||||
{
|
||||
gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_CENTER);
|
||||
}
|
||||
else
|
||||
{
|
||||
gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_MOUSE);
|
||||
}
|
||||
|
||||
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, TRUE, 0);
|
||||
|
||||
g_object_set_data (G_OBJECT (dialog), "cb", callback);
|
||||
@@ -592,7 +622,8 @@ fe_get_str (char *msg, char *def, void *callback, void *userdata)
|
||||
entry = gtk_entry_new ();
|
||||
g_signal_connect (G_OBJECT (entry), "activate",
|
||||
G_CALLBACK (gtkutil_str_enter), dialog);
|
||||
gtk_entry_set_text (GTK_ENTRY (entry), def);
|
||||
gtk_entry_set_text (GTK_ENTRY (entry), def ? def : "");
|
||||
gtk_entry_set_visibility (GTK_ENTRY (entry), visible);
|
||||
gtk_box_pack_end (GTK_BOX (hbox), entry, 0, 0, 0);
|
||||
|
||||
label = gtk_label_new (msg);
|
||||
@@ -606,6 +637,18 @@ fe_get_str (char *msg, char *def, void *callback, void *userdata)
|
||||
gtk_widget_show_all (dialog);
|
||||
}
|
||||
|
||||
void
|
||||
fe_get_str (char *msg, char *def, void *callback, void *userdata)
|
||||
{
|
||||
gtkutil_get_str (msg, def, callback, userdata, TRUE);
|
||||
}
|
||||
|
||||
void
|
||||
fe_get_password (char *msg, void *callback, void *userdata)
|
||||
{
|
||||
gtkutil_get_str (msg, "", callback, userdata, FALSE);
|
||||
}
|
||||
|
||||
static void
|
||||
gtkutil_get_number_response (GtkDialog *dialog, gint arg1, gpointer spin)
|
||||
{
|
||||
@@ -641,15 +684,15 @@ gtkutil_get_bool_response (GtkDialog *dialog, gint arg1, gpointer spin)
|
||||
|
||||
switch (arg1)
|
||||
{
|
||||
case GTK_RESPONSE_REJECT:
|
||||
callback (0, user_data);
|
||||
gtk_widget_destroy (GTK_WIDGET (dialog));
|
||||
break;
|
||||
case GTK_RESPONSE_ACCEPT:
|
||||
callback (1, user_data);
|
||||
gtk_widget_destroy (GTK_WIDGET (dialog));
|
||||
break;
|
||||
default:
|
||||
callback (0, user_data);
|
||||
break;
|
||||
}
|
||||
|
||||
gtk_widget_destroy (GTK_WIDGET (dialog));
|
||||
}
|
||||
|
||||
void
|
||||
@@ -667,9 +710,10 @@ fe_get_int (char *msg, int def, void *callback, void *userdata)
|
||||
_("_OK"), GTK_RESPONSE_ACCEPT,
|
||||
NULL);
|
||||
theme_manager_attach_window (dialog);
|
||||
if (parent_window && GTK_IS_WINDOW (parent_window))
|
||||
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
|
||||
gtkutil_place_dialog (dialog);
|
||||
gtk_box_set_homogeneous (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), TRUE);
|
||||
gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_MOUSE);
|
||||
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
|
||||
|
||||
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, TRUE, 0);
|
||||
|
||||
@@ -707,10 +751,10 @@ fe_get_bool (char *title, char *prompt, void *callback, void *userdata)
|
||||
_("_Yes"), GTK_RESPONSE_ACCEPT,
|
||||
NULL);
|
||||
theme_manager_attach_window (dialog);
|
||||
if (parent_window && GTK_IS_WINDOW (parent_window))
|
||||
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
|
||||
gtkutil_place_dialog (dialog);
|
||||
gtk_box_set_homogeneous (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), TRUE);
|
||||
gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_MOUSE);
|
||||
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
|
||||
|
||||
|
||||
g_object_set_data (G_OBJECT (dialog), "cb", callback);
|
||||
g_object_set_data (G_OBJECT (dialog), "ud", userdata);
|
||||
|
||||
@@ -67,6 +67,34 @@
|
||||
#include <shellapi.h>
|
||||
#include <gdk/gdkwin32.h>
|
||||
|
||||
/* The shell only minimizes a window from its taskbar button when the
|
||||
* window style carries WS_MINIMIZEBOX; without it the click merely
|
||||
* deactivates the window (issue #65). GTK3 older than 3.24.50 can drop
|
||||
* the bit from top-level windows (https://gitlab.gnome.org/GNOME/gtk/-/issues/7494),
|
||||
* so guarantee it ourselves for session windows. */
|
||||
static void
|
||||
mg_win32_ensure_minimizable (GdkWindow *gdk_window)
|
||||
{
|
||||
HWND hwnd;
|
||||
LONG_PTR window_style;
|
||||
|
||||
if (!gdk_window)
|
||||
return;
|
||||
|
||||
hwnd = gdk_win32_window_get_handle (gdk_window);
|
||||
if (!hwnd)
|
||||
return;
|
||||
|
||||
window_style = GetWindowLongPtr (hwnd, GWL_STYLE);
|
||||
if (!window_style || (window_style & WS_MINIMIZEBOX))
|
||||
return;
|
||||
|
||||
SetWindowLongPtr (hwnd, GWL_STYLE, window_style | WS_MINIMIZEBOX);
|
||||
SetWindowPos (hwnd, NULL, 0, 0, 0, 0,
|
||||
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE |
|
||||
SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOOWNERZORDER);
|
||||
}
|
||||
|
||||
static void
|
||||
mg_win32_allow_autohide_taskbar (GtkWindow *window, GdkEventWindowState *event)
|
||||
{
|
||||
@@ -173,6 +201,7 @@ enum
|
||||
#define TAG_UTIL 1 /* dcc, notify, chanlist */
|
||||
|
||||
static void mg_apply_emoji_fallback_widget (GtkWidget *widget);
|
||||
static void mg_reply_show_child (GtkWidget *widget, gpointer data);
|
||||
|
||||
#define MG_CONFIG_SAVE_DEBOUNCE_MS 250
|
||||
|
||||
@@ -731,6 +760,164 @@ mg_inputbox_focus (GtkWidget *widget, GdkEventFocus *event, session_gui *gui)
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
|
||||
static gboolean
|
||||
mg_client_tag_allowed (server *serv, const char *tag)
|
||||
{
|
||||
char **deny;
|
||||
int i;
|
||||
|
||||
if (!serv->have_message_tags)
|
||||
return FALSE;
|
||||
|
||||
if (!serv->clienttagdeny || !*serv->clienttagdeny)
|
||||
return TRUE;
|
||||
|
||||
deny = g_strsplit (serv->clienttagdeny, ",", 0);
|
||||
for (i = 0; deny[i]; i++)
|
||||
{
|
||||
if (!strcmp (deny[i], "*") || !strcmp (deny[i], tag) || (deny[i][0] == '+' && !strcmp (deny[i] + 1, tag)))
|
||||
{
|
||||
g_strfreev (deny);
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
g_strfreev (deny);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
mg_send_typing (session *sess, const char *state)
|
||||
{
|
||||
char tags[32];
|
||||
|
||||
if (!sess || !sess->server->connected || !mg_client_tag_allowed (sess->server, "typing") || !sess->channel[0])
|
||||
return;
|
||||
|
||||
if (sess->type != SESS_CHANNEL && sess->type != SESS_DIALOG)
|
||||
return;
|
||||
|
||||
g_snprintf (tags, sizeof (tags), "+typing=%s", state);
|
||||
sess->server->p_tagmsg (sess->server, tags, sess->channel);
|
||||
}
|
||||
|
||||
static int
|
||||
mg_typing_pause_cb (session *sess)
|
||||
{
|
||||
sess->typing_timeout_tag = 0;
|
||||
if (sess->typing_status == 1)
|
||||
{
|
||||
mg_send_typing (sess, "paused");
|
||||
sess->typing_status = 2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
mg_typing_update (session *sess, const char *text)
|
||||
{
|
||||
if (!sess)
|
||||
return;
|
||||
|
||||
if (sess->typing_timeout_tag)
|
||||
{
|
||||
fe_timeout_remove (sess->typing_timeout_tag);
|
||||
sess->typing_timeout_tag = 0;
|
||||
}
|
||||
|
||||
if (!text || !*text || text[0] == prefs.hex_input_command_char[0])
|
||||
{
|
||||
if (sess->typing_status)
|
||||
mg_send_typing (sess, "done");
|
||||
sess->typing_status = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sess->typing_status != 1)
|
||||
{
|
||||
mg_send_typing (sess, "active");
|
||||
sess->typing_status = 1;
|
||||
}
|
||||
sess->typing_timeout_tag = fe_timeout_add_seconds (6, mg_typing_pause_cb, sess);
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
mg_reply_show_child (GtkWidget *widget, gpointer data)
|
||||
{
|
||||
gtk_widget_show (widget);
|
||||
}
|
||||
|
||||
void
|
||||
mg_reply_update (session *sess)
|
||||
{
|
||||
char *nick;
|
||||
char *text;
|
||||
char *markup;
|
||||
|
||||
if (!sess || !sess->gui || !sess->gui->reply_box || !sess->gui->reply_label)
|
||||
return;
|
||||
|
||||
if (!sess->reply_msgid)
|
||||
{
|
||||
gtk_widget_hide (sess->gui->reply_box);
|
||||
return;
|
||||
}
|
||||
|
||||
nick = g_markup_escape_text (sess->reply_nick ? sess->reply_nick : _("message"), -1);
|
||||
text = g_markup_escape_text (sess->reply_text ? sess->reply_text : _("Original message unavailable"), -1);
|
||||
markup = g_strdup_printf ("<span foreground='#7d8790'>↪ Replying to <b>%s</b> · %.160s</span>", nick, text);
|
||||
gtk_label_set_markup (GTK_LABEL (sess->gui->reply_label), markup);
|
||||
gtk_container_foreach (GTK_CONTAINER (sess->gui->reply_box), mg_reply_show_child, NULL);
|
||||
gtk_widget_show (sess->gui->reply_box);
|
||||
g_free (markup);
|
||||
g_free (text);
|
||||
g_free (nick);
|
||||
}
|
||||
|
||||
static void
|
||||
mg_reply_cancel_cb (GtkWidget *wid, session *sess)
|
||||
{
|
||||
reply_state_clear (sess);
|
||||
mg_reply_update (sess);
|
||||
}
|
||||
|
||||
static void
|
||||
mg_send_reply_or_text (session *sess, char *cmd)
|
||||
{
|
||||
char *reply_cmd;
|
||||
|
||||
if (!sess->reply_msgid || cmd[0] == prefs.hex_input_command_char[0])
|
||||
{
|
||||
handle_multiline (sess, cmd, TRUE, FALSE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sess->server->connected || !mg_client_tag_allowed (sess->server, "reply"))
|
||||
{
|
||||
PrintText (sess, _("Replies are not supported on this server. Sending normally.\n"));
|
||||
reply_state_clear (sess);
|
||||
mg_reply_update (sess);
|
||||
handle_multiline (sess, cmd, TRUE, FALSE);
|
||||
return;
|
||||
}
|
||||
|
||||
reply_cmd = g_strdup_printf ("%cREPLY %s %s", prefs.hex_input_command_char[0], sess->reply_msgid, cmd);
|
||||
handle_multiline (sess, reply_cmd, TRUE, FALSE);
|
||||
g_free (reply_cmd);
|
||||
reply_state_clear (sess);
|
||||
mg_reply_update (sess);
|
||||
}
|
||||
|
||||
static void
|
||||
mg_inputbox_changed (GtkEditable *editable, session_gui *gui)
|
||||
{
|
||||
key_check_replace_on_change (editable, NULL);
|
||||
if (current_sess && current_sess->gui == gui)
|
||||
mg_typing_update (current_sess, gtk_entry_get_text (GTK_ENTRY (editable)));
|
||||
}
|
||||
|
||||
void
|
||||
mg_inputbox_cb (GtkWidget *igad, session_gui *gui)
|
||||
{
|
||||
@@ -772,7 +959,7 @@ mg_inputbox_cb (GtkWidget *igad, session_gui *gui)
|
||||
}
|
||||
|
||||
if (sess)
|
||||
handle_multiline (sess, cmd, TRUE, FALSE);
|
||||
mg_send_reply_or_text (sess, cmd);
|
||||
|
||||
g_free (cmd);
|
||||
}
|
||||
@@ -1003,7 +1190,9 @@ mg_windowstate_cb (GtkWindow *wid, GdkEventWindowState *event, gpointer userdata
|
||||
menu_set_fullscreen (current_sess->gui, prefs.hex_gui_win_fullscreen);
|
||||
|
||||
#ifdef G_OS_WIN32
|
||||
mg_win32_allow_autohide_taskbar (wid, event);
|
||||
if (event->changed_mask &
|
||||
(GDK_WINDOW_STATE_MAXIMIZED | GDK_WINDOW_STATE_FULLSCREEN))
|
||||
mg_win32_allow_autohide_taskbar (wid, event);
|
||||
#endif
|
||||
|
||||
return FALSE;
|
||||
@@ -3040,7 +3229,7 @@ mg_create_chanmodebuttons (session_gui *gui, GtkWidget *box)
|
||||
gui->key_entry = gtk_entry_new ();
|
||||
gtk_widget_set_name (gui->key_entry, "zoitechat-inputbox");
|
||||
gtk_entry_set_max_length (GTK_ENTRY (gui->key_entry), 23);
|
||||
gtk_widget_set_size_request (gui->key_entry, 115, 11);
|
||||
gtk_widget_set_size_request (gui->key_entry, 58, 11);
|
||||
gtk_box_pack_start (GTK_BOX (box), gui->key_entry, 0, 0, 0);
|
||||
mg_apply_emoji_fallback_widget (gui->key_entry);
|
||||
mg_apply_compact_mode_css (gui->key_entry);
|
||||
@@ -3057,7 +3246,8 @@ mg_create_chanmodebuttons (session_gui *gui, GtkWidget *box)
|
||||
gui->limit_entry = gtk_entry_new ();
|
||||
gtk_widget_set_name (gui->limit_entry, "zoitechat-inputbox");
|
||||
gtk_entry_set_max_length (GTK_ENTRY (gui->limit_entry), 10);
|
||||
gtk_widget_set_size_request (gui->limit_entry, 30, 11);
|
||||
gtk_entry_set_width_chars (GTK_ENTRY (gui->limit_entry), 5);
|
||||
gtk_widget_set_size_request (gui->limit_entry, 45, 11);
|
||||
gtk_box_pack_start (GTK_BOX (box), gui->limit_entry, 0, 0, 0);
|
||||
mg_apply_emoji_fallback_widget (gui->limit_entry);
|
||||
mg_apply_compact_mode_css (gui->limit_entry);
|
||||
@@ -3191,8 +3381,11 @@ mg_topicbar_update_height (GtkWidget *topic)
|
||||
width -= margin_left + margin_right;
|
||||
if (width < 1)
|
||||
width = 1;
|
||||
pango_layout_set_width (layout, width * PANGO_SCALE);
|
||||
pango_layout_set_wrap (layout, PANGO_WRAP_WORD_CHAR);
|
||||
if (prefs.hex_gui_topicbar_multiline && !prefs.hex_gui_mode_buttons_inline)
|
||||
{
|
||||
pango_layout_set_width (layout, width * PANGO_SCALE);
|
||||
pango_layout_set_wrap (layout, PANGO_WRAP_WORD_CHAR);
|
||||
}
|
||||
|
||||
context = gtk_widget_get_pango_context (topic);
|
||||
metrics = pango_context_get_metrics (context,
|
||||
@@ -3204,7 +3397,8 @@ mg_topicbar_update_height (GtkWidget *topic)
|
||||
if (line_height <= 0)
|
||||
line_height = 16;
|
||||
|
||||
line_count = pango_layout_get_line_count (layout);
|
||||
line_count = prefs.hex_gui_topicbar_multiline && !prefs.hex_gui_mode_buttons_inline ?
|
||||
pango_layout_get_line_count (layout) : 1;
|
||||
if (line_count <= 0)
|
||||
line_count = 1;
|
||||
|
||||
@@ -3222,10 +3416,19 @@ mg_topicbar_update_height (GtkWidget *topic)
|
||||
|
||||
if (parent && GTK_IS_SCROLLED_WINDOW (parent))
|
||||
{
|
||||
gtk_scrolled_window_set_max_content_height (GTK_SCROLLED_WINDOW (parent), -1);
|
||||
gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (parent), -1);
|
||||
gtk_scrolled_window_set_max_content_height (GTK_SCROLLED_WINDOW (parent), target_height);
|
||||
gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (parent), target_height);
|
||||
GtkScrolledWindow *scrolled = GTK_SCROLLED_WINDOW (parent);
|
||||
int max_height = gtk_scrolled_window_get_max_content_height (scrolled);
|
||||
|
||||
if (max_height != -1 && target_height > max_height)
|
||||
{
|
||||
gtk_scrolled_window_set_max_content_height (scrolled, target_height);
|
||||
gtk_scrolled_window_set_min_content_height (scrolled, target_height);
|
||||
}
|
||||
else
|
||||
{
|
||||
gtk_scrolled_window_set_min_content_height (scrolled, target_height);
|
||||
gtk_scrolled_window_set_max_content_height (scrolled, target_height);
|
||||
}
|
||||
gtk_widget_set_size_request (parent, -1, target_height);
|
||||
}
|
||||
}
|
||||
@@ -3326,7 +3529,7 @@ mg_apply_session_font_prefs (session_gui *gui)
|
||||
static void
|
||||
mg_create_topicbar (session *sess, GtkWidget *box)
|
||||
{
|
||||
GtkWidget *vbox, *hbox, *mode_hbox, *topic, *bbox;
|
||||
GtkWidget *vbox, *hbox, *mode_hbox, *topic, *topic_scroll, *bbox;
|
||||
session_gui *gui = sess->gui;
|
||||
|
||||
gui->topic_bar = vbox = mg_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 0);
|
||||
@@ -3340,7 +3543,9 @@ mg_create_topicbar (session *sess, GtkWidget *box)
|
||||
|
||||
gui->topic_entry = topic = gtk_text_view_new ();
|
||||
gtk_widget_set_name (topic, "zoitechat-topicbox");
|
||||
gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (topic), GTK_WRAP_WORD_CHAR);
|
||||
gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (topic),
|
||||
prefs.hex_gui_topicbar_multiline && !prefs.hex_gui_mode_buttons_inline ?
|
||||
GTK_WRAP_WORD_CHAR : GTK_WRAP_NONE);
|
||||
gtk_text_view_set_left_margin (GTK_TEXT_VIEW (topic), 4);
|
||||
gtk_text_view_set_right_margin (GTK_TEXT_VIEW (topic), 4);
|
||||
gtk_text_view_set_top_margin (GTK_TEXT_VIEW (topic), 4);
|
||||
@@ -3353,8 +3558,17 @@ mg_create_topicbar (session *sess, GtkWidget *box)
|
||||
G_CALLBACK (mg_topicbar_buffer_changed_cb), topic);
|
||||
g_signal_connect (G_OBJECT (topic), "size-allocate",
|
||||
G_CALLBACK (mg_topicbar_size_allocate_cb), NULL);
|
||||
topic_scroll = gtk_scrolled_window_new (NULL, NULL);
|
||||
gtk_widget_set_hexpand (topic_scroll, TRUE);
|
||||
gtk_widget_set_size_request (topic_scroll, 1, -1);
|
||||
gtk_widget_set_size_request (topic, 1, -1);
|
||||
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (topic_scroll),
|
||||
GTK_POLICY_EXTERNAL, GTK_POLICY_NEVER);
|
||||
gtk_scrolled_window_set_propagate_natural_width (GTK_SCROLLED_WINDOW (topic_scroll), FALSE);
|
||||
gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (topic_scroll), GTK_SHADOW_NONE);
|
||||
gtk_container_add (GTK_CONTAINER (topic_scroll), topic);
|
||||
mg_topicbar_update_height (topic);
|
||||
gtk_box_pack_start (GTK_BOX (hbox), topic, TRUE, TRUE, 0);
|
||||
gtk_box_pack_start (GTK_BOX (hbox), topic_scroll, TRUE, TRUE, 0);
|
||||
gtk_widget_add_events (topic, GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
|
||||
GDK_POINTER_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK);
|
||||
g_signal_connect (G_OBJECT (topic), "key-press-event",
|
||||
@@ -3371,9 +3585,13 @@ mg_create_topicbar (session *sess, GtkWidget *box)
|
||||
mg_create_dialogbuttons (bbox);
|
||||
|
||||
mode_hbox = mg_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 0);
|
||||
gtk_box_pack_start (GTK_BOX (vbox), mode_hbox, 0, 0, 0);
|
||||
if (prefs.hex_gui_mode_buttons_inline)
|
||||
gtk_box_pack_start (GTK_BOX (hbox), mode_hbox, 0, 0, 0);
|
||||
else
|
||||
gtk_box_pack_start (GTK_BOX (vbox), mode_hbox, 0, 0, 0);
|
||||
|
||||
gui->topicbutton_box = bbox = mg_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 0);
|
||||
gtk_widget_set_valign (bbox, GTK_ALIGN_CENTER);
|
||||
gtk_box_pack_end (GTK_BOX (mode_hbox), bbox, 0, 0, 0);
|
||||
mg_create_chanmodebuttons (gui, bbox);
|
||||
}
|
||||
@@ -4519,6 +4737,20 @@ mg_create_entry (session *sess, GtkWidget *box)
|
||||
};
|
||||
const char *emoji_fallback_icon_name;
|
||||
|
||||
gui->reply_box = mg_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6);
|
||||
gtk_widget_set_name (gui->reply_box, "zoitechat-replybar");
|
||||
gtk_widget_set_no_show_all (gui->reply_box, TRUE);
|
||||
gtk_box_pack_start (GTK_BOX (box), gui->reply_box, 0, 0, 0);
|
||||
gui->reply_label = gtk_label_new ("");
|
||||
gtk_label_set_ellipsize (GTK_LABEL (gui->reply_label), PANGO_ELLIPSIZE_END);
|
||||
gtk_box_pack_start (GTK_BOX (gui->reply_box), gui->reply_label, TRUE, TRUE, 8);
|
||||
but = gtk_button_new_with_label ("×");
|
||||
gtk_button_set_relief (GTK_BUTTON (but), GTK_RELIEF_NONE);
|
||||
gtk_widget_set_can_focus (but, FALSE);
|
||||
gtk_box_pack_start (GTK_BOX (gui->reply_box), but, FALSE, FALSE, 0);
|
||||
g_signal_connect (G_OBJECT (but), "clicked", G_CALLBACK (mg_reply_cancel_cb), sess);
|
||||
gtk_widget_hide (gui->reply_box);
|
||||
|
||||
hbox = mg_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 0);
|
||||
gtk_box_pack_start (GTK_BOX (box), hbox, 0, 0, 0);
|
||||
|
||||
@@ -4540,7 +4772,7 @@ mg_create_entry (session *sess, GtkWidget *box)
|
||||
g_signal_connect (G_OBJECT (entry), "activate",
|
||||
G_CALLBACK (mg_inputbox_cb), gui);
|
||||
g_signal_connect (G_OBJECT (entry), "changed",
|
||||
G_CALLBACK (key_check_replace_on_change), NULL);
|
||||
G_CALLBACK (mg_inputbox_changed), gui);
|
||||
gtk_box_pack_start (GTK_BOX (hbox), entry, TRUE, TRUE, 0);
|
||||
|
||||
gtk_widget_set_name (entry, "zoitechat-inputbox");
|
||||
@@ -4672,7 +4904,11 @@ mg_create_menu (session_gui *gui, GtkWidget *table, int away_state)
|
||||
gui->menu_item);
|
||||
gtk_widget_set_hexpand (gui->menu, TRUE);
|
||||
gtk_widget_set_vexpand (gui->menu, FALSE);
|
||||
gtk_widget_set_halign (gui->menu, GTK_ALIGN_START);
|
||||
/* Fill the row: with GTK_ALIGN_START the menu bar stops after its
|
||||
* last item and the rest of the row shows bare window background,
|
||||
* which reads as a two-colored menu bar whenever the theme styles
|
||||
* menubar and window differently. */
|
||||
gtk_widget_set_halign (gui->menu, GTK_ALIGN_FILL);
|
||||
gtk_widget_set_valign (gui->menu, GTK_ALIGN_FILL);
|
||||
gtk_grid_attach (GTK_GRID (table), gui->menu, 0, 0, 3, 1);
|
||||
}
|
||||
@@ -4781,6 +5017,7 @@ mg_create_topwindow (session *sess)
|
||||
#ifdef G_OS_WIN32
|
||||
parent_win = gtk_widget_get_window (win);
|
||||
gdk_window_add_filter (parent_win, mg_win32_filter, NULL);
|
||||
mg_win32_ensure_minimizable (parent_win);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -4817,6 +5054,18 @@ mg_win32_filter (GdkXEvent *xevent, GdkEvent *event, gpointer data)
|
||||
if (!msg)
|
||||
return GDK_FILTER_CONTINUE;
|
||||
|
||||
if (msg->message == WM_STYLECHANGING && msg->wParam == (WPARAM)GWL_STYLE)
|
||||
{
|
||||
STYLESTRUCT *style = (STYLESTRUCT *)msg->lParam;
|
||||
|
||||
/* Keep the window minimizable from its taskbar button; see
|
||||
* mg_win32_ensure_minimizable (). */
|
||||
if (style)
|
||||
style->styleNew |= WS_MINIMIZEBOX;
|
||||
|
||||
return GDK_FILTER_CONTINUE;
|
||||
}
|
||||
|
||||
if (msg->message == WM_TIMECHANGE)
|
||||
{
|
||||
_tzset();
|
||||
@@ -4966,6 +5215,7 @@ mg_create_tabwindow (session *sess)
|
||||
#ifdef G_OS_WIN32
|
||||
parent_win = gtk_widget_get_window (win);
|
||||
gdk_window_add_filter (parent_win, mg_win32_filter, NULL);
|
||||
mg_win32_ensure_minimizable (parent_win);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -5172,6 +5422,12 @@ fe_set_channel (session *sess)
|
||||
chan_rename (sess->res->tab, sess->channel, prefs.hex_gui_tab_trunc);
|
||||
}
|
||||
|
||||
void
|
||||
fe_set_typing (session *sess, const char *nick, const char *state)
|
||||
{
|
||||
fe_userlist_set_typing (sess, nick, state);
|
||||
}
|
||||
|
||||
void
|
||||
mg_changui_new (session *sess, restore_gui *res, int tab, int focus)
|
||||
{
|
||||
|
||||
@@ -48,6 +48,7 @@ void mg_dnd_drop_file (session *sess, char *target, char *uri);
|
||||
void mg_change_layout (int type);
|
||||
void mg_update_meters (session_gui *);
|
||||
void mg_inputbox_cb (GtkWidget *igad, session_gui *gui);
|
||||
void mg_reply_update (session *sess);
|
||||
void mg_create_icon_item (char *label, char *stock, GtkWidget *menu, void *callback, void *userdata);
|
||||
GtkWidget *mg_submenu (GtkWidget *menu, char *text);
|
||||
/* DND */
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
#include "../common/zoitechatc.h"
|
||||
#include "../common/cfgfiles.h"
|
||||
#include "../common/outbound.h"
|
||||
#include "../common/inbound.h"
|
||||
#include "../common/ignore.h"
|
||||
#include "../common/fe.h"
|
||||
#include "../common/server.h"
|
||||
@@ -784,6 +785,25 @@ fe_userlist_update (session *sess, struct User *user)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
menu_reply_to_latest_cb (GtkWidget *wid, gpointer data)
|
||||
{
|
||||
reply_item *item;
|
||||
|
||||
item = reply_cache_latest_from (current_sess, str_copy);
|
||||
if (!item)
|
||||
{
|
||||
PrintText (current_sess, _("No recent message to reply to.\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
reply_state_set (current_sess, item->msgid, current_sess->channel, item->nick, item->text);
|
||||
mg_reply_update (current_sess);
|
||||
if (current_sess->gui && current_sess->gui->input_box)
|
||||
gtk_widget_grab_focus (current_sess->gui->input_box);
|
||||
}
|
||||
|
||||
void
|
||||
menu_nickmenu (session *sess, GdkEventButton *event, char *nick, int num_sel)
|
||||
{
|
||||
@@ -827,6 +847,12 @@ menu_nickmenu (session *sess, GdkEventButton *event, char *nick, int num_sel)
|
||||
else
|
||||
menu_create (menu, popup_list, str_copy, FALSE);
|
||||
|
||||
if (num_sel <= 1)
|
||||
{
|
||||
menu_quick_item_with_callback (menu_reply_to_latest_cb, _("Reply"), menu, 0);
|
||||
menu_quick_item (0, 0, menu, XCMENU_SHADED, 0, 0);
|
||||
}
|
||||
|
||||
if (num_sel == 0) /* xtext click */
|
||||
menu_add_plugin_items (menu, "\x5$NICK", str_copy);
|
||||
else /* userlist treeview click */
|
||||
@@ -1755,44 +1781,6 @@ menu_change_layout (void)
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
menu_update_quit_accel (void)
|
||||
{
|
||||
GSList *list;
|
||||
|
||||
list = sess_list;
|
||||
while (list)
|
||||
{
|
||||
session *sess = list->data;
|
||||
session_gui *gui = sess->gui;
|
||||
GtkWidget *item;
|
||||
GtkAccelGroup *accel_group;
|
||||
int enabled;
|
||||
|
||||
list = list->next;
|
||||
if (!gui)
|
||||
continue;
|
||||
|
||||
item = gui->menu_item[MENU_ID_QUIT];
|
||||
if (!item)
|
||||
continue;
|
||||
|
||||
enabled = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (item), "zc-ctrlq-enabled"));
|
||||
if (enabled == (int)prefs.hex_gui_ctrlq_quit)
|
||||
continue;
|
||||
|
||||
accel_group = g_object_get_data (G_OBJECT (item), "zc-quit-accel-group");
|
||||
if (!accel_group)
|
||||
continue;
|
||||
|
||||
if (prefs.hex_gui_ctrlq_quit)
|
||||
gtk_widget_add_accelerator (item, "activate", accel_group, GDK_KEY_q, STATE_CTRL, GTK_ACCEL_VISIBLE);
|
||||
else
|
||||
gtk_widget_remove_accelerator (item, accel_group, GDK_KEY_q, STATE_CTRL);
|
||||
g_object_set_data (G_OBJECT (item), "zc-ctrlq-enabled", GINT_TO_POINTER (prefs.hex_gui_ctrlq_quit));
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
menu_layout_cb (GtkWidget *item, gpointer none)
|
||||
{
|
||||
@@ -1939,13 +1927,13 @@ menu_about (GtkWidget *wid, gpointer sess)
|
||||
|
||||
static struct mymenu mymenu[] = {
|
||||
{N_("_ZoiteChat"), 0, 0, M_NEWMENU, MENU_ID_ZOITECHAT, 0, 1},
|
||||
{N_("Network Li_st"), menu_open_server_list, 0, M_MENUITEM, 0, 0, 1, GDK_KEY_s},
|
||||
{N_("Network Li_st"), menu_open_server_list, 0, M_MENUITEM, 0, 0, 1},
|
||||
{0, 0, 0, M_SEP, 0, 0, 0},
|
||||
|
||||
{N_("_New"), 0, 0, M_MENUSUB, 0, 0, 1},
|
||||
{N_("Server Tab"), menu_newserver_tab, 0, M_MENUITEM, 0, 0, 1, GDK_KEY_t},
|
||||
{N_("Server Tab"), menu_newserver_tab, 0, M_MENUITEM, 0, 0, 1},
|
||||
{N_("Channel Tab"), menu_newchannel_tab, 0, M_MENUITEM, 0, 0, 1},
|
||||
{N_("Server Window"), menu_newserver_window, 0, M_MENUITEM, 0, 0, 1, GDK_KEY_n},
|
||||
{N_("Server Window"), menu_newserver_window, 0, M_MENUITEM, 0, 0, 1},
|
||||
{N_("Channel Window"), menu_newchannel_window, 0, M_MENUITEM, 0, 0, 1},
|
||||
{0, 0, 0, M_END, 0, 0, 0},
|
||||
{0, 0, 0, M_SEP, 0, 0, 0},
|
||||
@@ -1957,13 +1945,13 @@ static struct mymenu mymenu[] = {
|
||||
#define CLOSE_OFFSET (13)
|
||||
{0, menu_close, 0, M_MENUITEM, 0, 0, 1},
|
||||
{0, 0, 0, M_SEP, 0, 0, 0},
|
||||
{N_("_Quit"), menu_quit, 0, M_MENUITEM, MENU_ID_QUIT, 0, 1, GDK_KEY_q}, /* 15 */
|
||||
{N_("_Quit"), menu_quit, 0, M_MENUITEM, MENU_ID_QUIT, 0, 1}, /* 15 */
|
||||
|
||||
{N_("_View"), 0, 0, M_NEWMENU, 0, 0, 1},
|
||||
#define MENUBAR_OFFSET (17)
|
||||
{N_("_Menu Bar"), menu_bar_toggle_cb, 0, M_MENUTOG, MENU_ID_MENUBAR, 0, 1, GDK_KEY_F9},
|
||||
{N_("_Menu Bar"), menu_bar_toggle_cb, 0, M_MENUTOG, MENU_ID_MENUBAR, 0, 1},
|
||||
{N_("_Topic Bar"), menu_topicbar_toggle, 0, M_MENUTOG, MENU_ID_TOPICBAR, 0, 1},
|
||||
{N_("_User List"), menu_userlist_toggle, 0, M_MENUTOG, MENU_ID_USERLIST, 0, 1, GDK_KEY_F7},
|
||||
{N_("_User List"), menu_userlist_toggle, 0, M_MENUTOG, MENU_ID_USERLIST, 0, 1},
|
||||
{N_("U_ser List Buttons"), menu_ulbuttons_toggle, 0, M_MENUTOG, MENU_ID_ULBUTTONS, 0, 1},
|
||||
{N_("M_ode Buttons"), menu_cmbuttons_toggle, 0, M_MENUTOG, MENU_ID_MODEBUTTONS, 0, 1},
|
||||
{0, 0, 0, M_SEP, 0, 0, 0},
|
||||
@@ -1980,7 +1968,7 @@ static struct mymenu mymenu[] = {
|
||||
{N_("Both"), menu_metres_both, 0, M_MENURADIO, 0, 0, 1},
|
||||
{0, 0, 0, M_END, 0, 0, 0}, /* 32 */
|
||||
{ 0, 0, 0, M_SEP, 0, 0, 0 },
|
||||
{N_ ("_Fullscreen"), menu_fullscreen_toggle, 0, M_MENUTOG, MENU_ID_FULLSCREEN, 0, 1, GDK_KEY_F11},
|
||||
{N_ ("_Fullscreen"), menu_fullscreen_toggle, 0, M_MENUTOG, MENU_ID_FULLSCREEN, 0, 1},
|
||||
|
||||
{N_("_Server"), 0, 0, M_NEWMENU, 0, 0, 1},
|
||||
{N_("_Disconnect"), menu_disconnect, 0, M_MENUITEM, MENU_ID_DISCONNECT, 0, 1},
|
||||
@@ -1989,7 +1977,7 @@ static struct mymenu mymenu[] = {
|
||||
{N_("Channel _List"), menu_chanlist, 0, M_MENUITEM, 0, 0, 1},
|
||||
{0, 0, 0, M_SEP, 0, 0, 0},
|
||||
#define AWAY_OFFSET (41)
|
||||
{N_("Marked _Away"), menu_away_toggle, 0, M_MENUITEM, MENU_ID_AWAY, 0, 1, GDK_KEY_a},
|
||||
{N_("Marked _Away"), menu_away_toggle, 0, M_MENUITEM, MENU_ID_AWAY, 0, 1},
|
||||
|
||||
{N_("_Usermenu"), 0, 0, M_NEWMENU, MENU_ID_USERMENU, 0, 1}, /* 40 */
|
||||
|
||||
@@ -2017,25 +2005,191 @@ static struct mymenu mymenu[] = {
|
||||
{N_("_Raw Log"), menu_rawlog, 0, M_MENUITEM, 0, 0, 1}, /* 61 */
|
||||
{N_("_URL Grabber"), url_opengui, 0, M_MENUITEM, 0, 0, 1},
|
||||
{0, 0, 0, M_SEP, 0, 0, 0},
|
||||
{N_("Reset Marker Line"), menu_resetmarker, 0, M_MENUITEM, 0, 0, 1, GDK_KEY_m},
|
||||
{N_("Move to Marker Line"), menu_movetomarker, 0, M_MENUITEM, 0, 0, 1, GDK_KEY_M},
|
||||
{N_("_Copy Selection"), menu_copy_selection, 0, M_MENUITEM, 0, 0, 1, GDK_KEY_C},
|
||||
{N_("Reset Marker Line"), menu_resetmarker, 0, M_MENUITEM, 0, 0, 1},
|
||||
{N_("Move to Marker Line"), menu_movetomarker, 0, M_MENUITEM, 0, 0, 1},
|
||||
{N_("_Copy Selection"), menu_copy_selection, 0, M_MENUITEM, 0, 0, 1},
|
||||
{N_("C_lear Text"), menu_flushbuffer, 0, M_MENUITEM, 0, 0, 1},
|
||||
{N_("Save Text" ELLIPSIS), menu_savebuffer, 0, M_MENUITEM, 0, 0, 1},
|
||||
#define SEARCH_OFFSET (70)
|
||||
{N_("Search"), 0, 0, M_MENUSUB, 0, 0, 1},
|
||||
{N_("Search Text" ELLIPSIS), menu_search, 0, M_MENUITEM, 0, 0, 1, GDK_KEY_f},
|
||||
{N_("Search Next" ), menu_search_next, 0, M_MENUITEM, 0, 0, 1, GDK_KEY_g},
|
||||
{N_("Search Previous" ), menu_search_prev, 0, M_MENUITEM, 0, 0, 1, GDK_KEY_G},
|
||||
{N_("Search Text" ELLIPSIS), menu_search, 0, M_MENUITEM, 0, 0, 1},
|
||||
{N_("Search Next" ), menu_search_next, 0, M_MENUITEM, 0, 0, 1},
|
||||
{N_("Search Previous" ), menu_search_prev, 0, M_MENUITEM, 0, 0, 1},
|
||||
{0, 0, 0, M_END, 0, 0, 0},
|
||||
|
||||
{N_("_Help"), 0, 0, M_NEWMENU, 0, 0, 1}, /* 74 */
|
||||
{N_("_Contents"), menu_docs, 0, M_MENUITEM, 0, 0, 1, GDK_KEY_F1},
|
||||
{N_("_Contents"), menu_docs, 0, M_MENUITEM, 0, 0, 1},
|
||||
{N_("_About"), menu_about, 0, M_MENUITEM, 0, 0, 1},
|
||||
|
||||
{0, 0, 0, M_END, 0, 0, 0},
|
||||
};
|
||||
|
||||
static const char *
|
||||
menu_get_key_action_name (int index)
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case 1:
|
||||
return "network-list";
|
||||
case 4:
|
||||
return "new-server-tab";
|
||||
case 6:
|
||||
return "new-server-window";
|
||||
case CLOSE_OFFSET:
|
||||
return "close";
|
||||
case 15:
|
||||
return "quit";
|
||||
case MENUBAR_OFFSET:
|
||||
return "menu-toggle";
|
||||
case MENUBAR_OFFSET + 2:
|
||||
return "user-list-toggle";
|
||||
case 34:
|
||||
return "fullscreen-toggle";
|
||||
case AWAY_OFFSET:
|
||||
return "away-toggle";
|
||||
case 65:
|
||||
return "reset-marker";
|
||||
case 66:
|
||||
return "move-marker";
|
||||
case 67:
|
||||
return "copy-selection";
|
||||
case SEARCH_OFFSET + 1:
|
||||
return "search-text";
|
||||
case SEARCH_OFFSET + 2:
|
||||
return "search-next";
|
||||
case SEARCH_OFFSET + 3:
|
||||
return "search-previous";
|
||||
case 75:
|
||||
return "contents";
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void
|
||||
menu_add_keybinding_accel (GtkWidget *item, GtkAccelGroup *accel_group, const char *name)
|
||||
{
|
||||
guint keyval;
|
||||
GdkModifierType mod;
|
||||
|
||||
if (!accel_group || !key_get_menu_accel (name, &keyval, &mod))
|
||||
return;
|
||||
|
||||
if (!strcmp (name, "quit") && !prefs.hex_gui_ctrlq_quit && keyval == GDK_KEY_q && mod == STATE_CTRL)
|
||||
return;
|
||||
|
||||
gtk_widget_add_accelerator (item, "activate", accel_group, keyval, mod, GTK_ACCEL_VISIBLE);
|
||||
g_object_set_data (G_OBJECT (item), "zc-key-accel-key", GUINT_TO_POINTER (keyval));
|
||||
g_object_set_data (G_OBJECT (item), "zc-key-accel-mod", GUINT_TO_POINTER (mod));
|
||||
}
|
||||
|
||||
static void
|
||||
menu_refresh_keybinding_accels (GtkWidget *widget, gpointer data)
|
||||
{
|
||||
GtkAccelGroup *accel_group = data;
|
||||
const char *name;
|
||||
guint keyval;
|
||||
GdkModifierType mod;
|
||||
GtkWidget *submenu;
|
||||
GList *children, *list;
|
||||
|
||||
if (GTK_IS_MENU_ITEM (widget))
|
||||
{
|
||||
keyval = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (widget), "zc-key-accel-key"));
|
||||
mod = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (widget), "zc-key-accel-mod"));
|
||||
if (keyval != 0)
|
||||
{
|
||||
gtk_widget_remove_accelerator (widget, accel_group, keyval, mod);
|
||||
g_object_set_data (G_OBJECT (widget), "zc-key-accel-key", NULL);
|
||||
g_object_set_data (G_OBJECT (widget), "zc-key-accel-mod", NULL);
|
||||
}
|
||||
|
||||
name = g_object_get_data (G_OBJECT (widget), "zc-key-action");
|
||||
menu_add_keybinding_accel (widget, accel_group, name);
|
||||
|
||||
submenu = gtk_menu_item_get_submenu (GTK_MENU_ITEM (widget));
|
||||
if (submenu)
|
||||
menu_refresh_keybinding_accels (submenu, data);
|
||||
}
|
||||
|
||||
if (GTK_IS_CONTAINER (widget))
|
||||
{
|
||||
children = gtk_container_get_children (GTK_CONTAINER (widget));
|
||||
for (list = children; list; list = g_list_next (list))
|
||||
menu_refresh_keybinding_accels (list->data, data);
|
||||
g_list_free (children);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
menu_update_quit_accel (void)
|
||||
{
|
||||
session *sess;
|
||||
GSList *list;
|
||||
GtkAccelGroup *accel_group;
|
||||
|
||||
list = sess_list;
|
||||
while (list)
|
||||
{
|
||||
sess = list->data;
|
||||
if (sess && sess->gui && sess->gui->menu)
|
||||
{
|
||||
accel_group = g_object_get_data (G_OBJECT (sess->gui->menu), "accel");
|
||||
if (accel_group)
|
||||
menu_refresh_keybinding_accels (sess->gui->menu, accel_group);
|
||||
}
|
||||
list = g_slist_next (list);
|
||||
}
|
||||
}
|
||||
|
||||
gboolean
|
||||
menu_key_action (const char *name, guint keyval, GdkModifierType state)
|
||||
{
|
||||
if (!name)
|
||||
return FALSE;
|
||||
|
||||
if (!strcmp (name, "network-list"))
|
||||
menu_open_server_list (NULL, NULL);
|
||||
else if (!strcmp (name, "new-server-tab"))
|
||||
menu_newserver_tab (NULL, NULL);
|
||||
else if (!strcmp (name, "new-server-window"))
|
||||
menu_newserver_window (NULL, NULL);
|
||||
else if (!strcmp (name, "close"))
|
||||
menu_close (NULL, NULL);
|
||||
else if (!strcmp (name, "quit"))
|
||||
{
|
||||
if (!prefs.hex_gui_ctrlq_quit && keyval == GDK_KEY_q && (state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_MOD1_MASK)) == STATE_CTRL)
|
||||
return FALSE;
|
||||
menu_quit (NULL, NULL);
|
||||
}
|
||||
else if (!strcmp (name, "menu-toggle"))
|
||||
menu_bar_toggle_cb ();
|
||||
else if (!strcmp (name, "user-list-toggle"))
|
||||
menu_userlist_toggle (NULL, NULL);
|
||||
else if (!strcmp (name, "fullscreen-toggle"))
|
||||
menu_fullscreen_toggle (NULL, NULL);
|
||||
else if (!strcmp (name, "away-toggle"))
|
||||
menu_away_toggle (NULL, NULL);
|
||||
else if (!strcmp (name, "reset-marker"))
|
||||
menu_resetmarker (NULL, NULL);
|
||||
else if (!strcmp (name, "move-marker"))
|
||||
menu_movetomarker (NULL, NULL);
|
||||
else if (!strcmp (name, "copy-selection"))
|
||||
menu_copy_selection (NULL, NULL);
|
||||
else if (!strcmp (name, "search-text"))
|
||||
menu_search ();
|
||||
else if (!strcmp (name, "search-next"))
|
||||
menu_search_next (NULL);
|
||||
else if (!strcmp (name, "search-previous"))
|
||||
menu_search_prev (NULL);
|
||||
else if (!strcmp (name, "contents"))
|
||||
menu_docs (NULL, NULL);
|
||||
else
|
||||
return FALSE;
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void
|
||||
menu_set_away (session_gui *gui, int away)
|
||||
{
|
||||
@@ -2641,6 +2795,8 @@ menu_create_main (void *accel_group, int bar, int away, int toplevel,
|
||||
case M_MENUITEM:
|
||||
item = gtk_menu_item_new_with_mnemonic (_(mymenu[i].text));
|
||||
normalitem:
|
||||
g_object_set_data (G_OBJECT (item), "zc-key-action", (gpointer) menu_get_key_action_name (i));
|
||||
menu_add_keybinding_accel (item, accel_group, menu_get_key_action_name (i));
|
||||
if (mymenu[i].key != 0 && !(mymenu[i].id == MENU_ID_QUIT && !prefs.hex_gui_ctrlq_quit))
|
||||
gtk_widget_add_accelerator (item, "activate", accel_group,
|
||||
mymenu[i].key,
|
||||
@@ -2669,6 +2825,8 @@ normalitem:
|
||||
case M_MENUTOG:
|
||||
item = gtk_check_menu_item_new_with_mnemonic (_(mymenu[i].text));
|
||||
togitem:
|
||||
g_object_set_data (G_OBJECT (item), "zc-key-action", (gpointer) menu_get_key_action_name (i));
|
||||
menu_add_keybinding_accel (item, accel_group, menu_get_key_action_name (i));
|
||||
/* must avoid callback for Radio buttons */
|
||||
gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (item), mymenu[i].state);
|
||||
/*gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (item),
|
||||
|
||||
@@ -39,6 +39,7 @@ void menu_bar_toggle (void);
|
||||
void menu_add_plugin_items (GtkWidget *menu, char *root, char *target);
|
||||
void menu_change_layout (void);
|
||||
void menu_update_quit_accel (void);
|
||||
gboolean menu_key_action (const char *name, guint keyval, GdkModifierType state);
|
||||
|
||||
void menu_set_away (session_gui *gui, int away);
|
||||
void menu_set_fullscreen (session_gui *gui, int fullscreen);
|
||||
|
||||
@@ -50,7 +50,7 @@ zoitechat_gtk_deps = [
|
||||
|
||||
gtk_dep = dependency('gtk+-3.0', version: '>= 3.22')
|
||||
|
||||
zoitechat_theme_deps = [gtk_dep]
|
||||
zoitechat_theme_deps = [gtk_dep, zoitechat_common_dep]
|
||||
|
||||
if host_machine.system() != 'windows'
|
||||
appindicator_opt = get_option('appindicator')
|
||||
@@ -241,7 +241,6 @@ test('Theme GTK3 Settings Tests', theme_gtk3_settings_tests,
|
||||
timeout: 120,
|
||||
)
|
||||
|
||||
|
||||
theme_preferences_gtk3_populate_tests = executable('theme_preferences_gtk3_populate_tests',
|
||||
'theme/tests/test-theme-preferences-gtk3-populate.c',
|
||||
include_directories: [config_h_include],
|
||||
|
||||
@@ -48,6 +48,9 @@ typedef struct _GtkStatusIcon GtkStatusIcon;
|
||||
|
||||
#ifndef WIN32
|
||||
#include <unistd.h>
|
||||
#else
|
||||
#include <windows.h>
|
||||
#include <gdk/gdkwin32.h>
|
||||
#endif
|
||||
|
||||
typedef enum /* current icon status */
|
||||
@@ -855,6 +858,7 @@ tray_toggle_visibility (gboolean force_hide)
|
||||
static int maximized;
|
||||
static int fullscreen;
|
||||
GtkWindow *win;
|
||||
WinStatus status;
|
||||
|
||||
if (!tray_backend_active)
|
||||
return FALSE;
|
||||
@@ -870,7 +874,9 @@ tray_toggle_visibility (gboolean force_hide)
|
||||
if (!win)
|
||||
return FALSE;
|
||||
|
||||
if (force_hide || gtk_widget_get_visible (GTK_WIDGET (win)))
|
||||
status = tray_get_window_status ();
|
||||
|
||||
if (force_hide || status != WS_HIDDEN)
|
||||
{
|
||||
if (prefs.hex_gui_tray_away)
|
||||
zoitechat_command (ph, "ALLSERV AWAY");
|
||||
@@ -890,8 +896,8 @@ tray_toggle_visibility (gboolean force_hide)
|
||||
gtk_window_maximize (win);
|
||||
if (fullscreen)
|
||||
gtk_window_fullscreen (win);
|
||||
gtk_widget_show (GTK_WIDGET (win));
|
||||
gtk_window_deiconify (win);
|
||||
gtk_widget_show (GTK_WIDGET (win));
|
||||
gtk_window_present (win);
|
||||
}
|
||||
|
||||
@@ -1089,6 +1095,119 @@ tray_menu_settings (GtkWidget * wid, gpointer none)
|
||||
setup_open ();
|
||||
}
|
||||
|
||||
#ifdef WIN32
|
||||
#define TRAY_WIN32_HIDE 1
|
||||
#define TRAY_WIN32_AWAY 2
|
||||
#define TRAY_WIN32_BACK 3
|
||||
#define TRAY_WIN32_PREFS 4
|
||||
#define TRAY_WIN32_QUIT 5
|
||||
|
||||
static WCHAR *
|
||||
tray_win32_menu_label (const char *label)
|
||||
{
|
||||
char *plain;
|
||||
char *src;
|
||||
char *dst;
|
||||
WCHAR *wide;
|
||||
|
||||
plain = g_strdup (label ? label : "");
|
||||
for (src = plain, dst = plain; *src; src++)
|
||||
{
|
||||
if (*src == '_')
|
||||
continue;
|
||||
*dst++ = *src;
|
||||
}
|
||||
*dst = 0;
|
||||
wide = g_utf8_to_utf16 (plain, -1, NULL, NULL, NULL);
|
||||
g_free (plain);
|
||||
|
||||
return wide;
|
||||
}
|
||||
|
||||
static void
|
||||
tray_win32_append_item (HMENU menu, UINT id, const char *label, gboolean enabled)
|
||||
{
|
||||
WCHAR *wide;
|
||||
|
||||
wide = tray_win32_menu_label (label);
|
||||
AppendMenuW (menu, MF_STRING | (enabled ? MF_ENABLED : MF_GRAYED), id, wide);
|
||||
g_free (wide);
|
||||
}
|
||||
|
||||
static HWND
|
||||
tray_win32_get_hwnd (void)
|
||||
{
|
||||
GtkWindow *win;
|
||||
GdkWindow *gdk_win;
|
||||
|
||||
win = GTK_WINDOW (zoitechat_get_info (ph, "gtkwin_ptr"));
|
||||
if (!win)
|
||||
return GetActiveWindow ();
|
||||
|
||||
gdk_win = gtk_widget_get_window (GTK_WIDGET (win));
|
||||
if (!gdk_win)
|
||||
return GetActiveWindow ();
|
||||
|
||||
return gdk_win32_window_get_handle (gdk_win);
|
||||
}
|
||||
|
||||
static void
|
||||
tray_win32_menu_cb (void)
|
||||
{
|
||||
HMENU menu;
|
||||
POINT point;
|
||||
UINT command;
|
||||
HWND hwnd;
|
||||
int away_status;
|
||||
|
||||
zoitechat_set_context (ph, zoitechat_find_context (ph, NULL, NULL));
|
||||
|
||||
menu = CreatePopupMenu ();
|
||||
if (!menu)
|
||||
return;
|
||||
|
||||
away_status = tray_find_away_status ();
|
||||
tray_win32_append_item (menu, TRAY_WIN32_HIDE,
|
||||
tray_get_window_status () == WS_HIDDEN ? _("_Restore Window") : _("_Hide Window"), TRUE);
|
||||
AppendMenuW (menu, MF_SEPARATOR, 0, NULL);
|
||||
tray_win32_append_item (menu, TRAY_WIN32_AWAY, _("_Away"), away_status != 1);
|
||||
tray_win32_append_item (menu, TRAY_WIN32_BACK, _("_Back"), away_status != 2);
|
||||
AppendMenuW (menu, MF_SEPARATOR, 0, NULL);
|
||||
tray_win32_append_item (menu, TRAY_WIN32_PREFS, _("_Preferences"), TRUE);
|
||||
AppendMenuW (menu, MF_SEPARATOR, 0, NULL);
|
||||
tray_win32_append_item (menu, TRAY_WIN32_QUIT, _("_Quit"), TRUE);
|
||||
|
||||
GetCursorPos (&point);
|
||||
hwnd = tray_win32_get_hwnd ();
|
||||
if (hwnd)
|
||||
SetForegroundWindow (hwnd);
|
||||
command = TrackPopupMenu (menu, TPM_RIGHTBUTTON | TPM_RETURNCMD | TPM_NONOTIFY,
|
||||
point.x, point.y, 0, hwnd, NULL);
|
||||
DestroyMenu (menu);
|
||||
|
||||
switch (command)
|
||||
{
|
||||
case TRAY_WIN32_HIDE:
|
||||
tray_toggle_visibility (FALSE);
|
||||
break;
|
||||
case TRAY_WIN32_AWAY:
|
||||
tray_foreach_server (NULL, "away");
|
||||
break;
|
||||
case TRAY_WIN32_BACK:
|
||||
tray_foreach_server (NULL, "back");
|
||||
break;
|
||||
case TRAY_WIN32_PREFS:
|
||||
tray_menu_settings (NULL, NULL);
|
||||
break;
|
||||
case TRAY_WIN32_QUIT:
|
||||
tray_menu_quit_cb (NULL, NULL);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
static void
|
||||
tray_menu_populate (GtkWidget *menu)
|
||||
{
|
||||
@@ -1216,6 +1335,15 @@ tray_window_visibility_cb (GtkWidget *widget, gpointer userdata)
|
||||
static void
|
||||
tray_menu_cb (GtkWidget *widget, guint button, guint time, gpointer userdata)
|
||||
{
|
||||
#ifdef WIN32
|
||||
(void)widget;
|
||||
(void)button;
|
||||
(void)time;
|
||||
(void)userdata;
|
||||
|
||||
tray_win32_menu_cb ();
|
||||
return;
|
||||
#else
|
||||
static GtkWidget *menu;
|
||||
|
||||
(void)button;
|
||||
@@ -1255,6 +1383,7 @@ tray_menu_cb (GtkWidget *widget, guint button, guint time, gpointer userdata)
|
||||
if (event)
|
||||
gdk_event_free (event);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1410,6 +1539,11 @@ tray_cleanup (void)
|
||||
void
|
||||
tray_apply_setup (void)
|
||||
{
|
||||
/* Theme/setup events can be dispatched before the tray plugin has
|
||||
* been registered; there is nothing to (de)activate yet. */
|
||||
if (!ph)
|
||||
return;
|
||||
|
||||
if (tray_backend_active)
|
||||
{
|
||||
if (!prefs.hex_gui_tray)
|
||||
|
||||
@@ -88,6 +88,7 @@ static GtkWidget *edit_entry_real;
|
||||
static GtkWidget *edit_entry_pass;
|
||||
static GtkWidget *edit_check_show_pass;
|
||||
static GtkWidget *edit_check_use_keyring;
|
||||
static GtkWidget *edit_check_ask_pass;
|
||||
static GtkWidget *edit_button_encrypt_pass;
|
||||
static GtkWidget *edit_button_import_pass;
|
||||
static int edit_pass_changed;
|
||||
@@ -122,7 +123,11 @@ servlist_update_password_tools (ircnet *net)
|
||||
return;
|
||||
|
||||
use_keyring = net && (net->flags & FLAG_USE_KEYRING);
|
||||
has_local = net && net->pass && *net->pass && !use_keyring && !edit_pass_changed;
|
||||
if ((net && (net->flags & FLAG_PROMPT_PASSWORD)) ||
|
||||
(edit_check_ask_pass && gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (edit_check_ask_pass))))
|
||||
use_keyring = FALSE;
|
||||
has_local = net && net->pass && *net->pass && !use_keyring && !edit_pass_changed &&
|
||||
(!edit_check_ask_pass || !gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (edit_check_ask_pass)));
|
||||
gtk_widget_set_sensitive (edit_button_encrypt_pass, has_local && !servlist_password_is_encrypted (net->pass));
|
||||
gtk_widget_set_sensitive (edit_button_import_pass, has_local);
|
||||
}
|
||||
@@ -135,6 +140,17 @@ servlist_entry_set_text_silent (GtkWidget *entry, const char *text)
|
||||
g_signal_handlers_unblock_by_func (G_OBJECT (entry), G_CALLBACK (servlist_password_changed_cb), NULL);
|
||||
}
|
||||
|
||||
static void
|
||||
servlist_toggle_ask_pass_cb (GtkToggleButton *toggle, gpointer userdata)
|
||||
{
|
||||
gboolean active = gtk_toggle_button_get_active (toggle);
|
||||
|
||||
gtk_widget_set_sensitive (edit_check_use_keyring, !active);
|
||||
gtk_widget_set_sensitive (edit_entry_pass, !active && (!selected_net || selected_net->logintype != LOGIN_SASLEXTERNAL));
|
||||
gtk_widget_set_sensitive (edit_check_show_pass, !active);
|
||||
servlist_update_password_tools (selected_net);
|
||||
}
|
||||
|
||||
static char *
|
||||
servlist_display_password (ircnet *net)
|
||||
{
|
||||
@@ -1261,6 +1277,7 @@ static void
|
||||
servlist_edit_update (ircnet *net)
|
||||
{
|
||||
gboolean use_keyring;
|
||||
gboolean ask_pass;
|
||||
gboolean keyring_changed;
|
||||
char *password = NULL;
|
||||
servlist_update_from_entry (&net->nick, edit_entry_nick);
|
||||
@@ -1269,8 +1286,23 @@ servlist_edit_update (ircnet *net)
|
||||
servlist_update_from_entry (&net->real, edit_entry_real);
|
||||
if (net && net->name)
|
||||
{
|
||||
use_keyring = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (edit_check_use_keyring));
|
||||
ask_pass = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (edit_check_ask_pass));
|
||||
use_keyring = !ask_pass && gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (edit_check_use_keyring));
|
||||
keyring_changed = !!(net->flags & FLAG_USE_KEYRING) != !!use_keyring;
|
||||
if (ask_pass)
|
||||
{
|
||||
secretstore_delete_network_password (net->name);
|
||||
if (net->pass)
|
||||
{
|
||||
memset (net->pass, 0, strlen (net->pass));
|
||||
g_free (net->pass);
|
||||
net->pass = NULL;
|
||||
}
|
||||
net->flags &= ~FLAG_USE_KEYRING;
|
||||
net->flags |= FLAG_PROMPT_PASSWORD;
|
||||
return;
|
||||
}
|
||||
net->flags &= ~FLAG_PROMPT_PASSWORD;
|
||||
if (!edit_pass_changed && !keyring_changed)
|
||||
return;
|
||||
if (edit_pass_changed)
|
||||
@@ -1338,6 +1370,7 @@ servlist_edit_close_cb (GtkWidget *button, gpointer userdata)
|
||||
edit_win = NULL;
|
||||
edit_entry_pass = NULL;
|
||||
edit_check_show_pass = NULL;
|
||||
edit_check_ask_pass = NULL;
|
||||
edit_button_encrypt_pass = NULL;
|
||||
edit_button_import_pass = NULL;
|
||||
}
|
||||
@@ -2565,7 +2598,7 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
|
||||
|
||||
|
||||
/* Checkboxes and entries */
|
||||
table3 = gtkutil_grid_new (17, 2, FALSE);
|
||||
table3 = gtkutil_grid_new (19, 2, FALSE);
|
||||
gtk_box_pack_start (GTK_BOX (vbox5), table3, FALSE, FALSE, 0);
|
||||
gtk_grid_set_row_spacing (GTK_GRID (table3), 2);
|
||||
gtk_grid_set_column_spacing (GTK_GRID (table3), 8);
|
||||
@@ -2584,34 +2617,43 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
|
||||
#endif
|
||||
servlist_create_check (1, net->flags & FLAG_USE_GLOBAL, table3, 5, 0, _("Use global user information"));
|
||||
|
||||
edit_check_ask_pass = gtk_check_button_new_with_mnemonic (_("Ask for password on connect"));
|
||||
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (edit_check_ask_pass), net->flags & FLAG_PROMPT_PASSWORD);
|
||||
servlist_table_attach (table3, edit_check_ask_pass, 0, 2, 6, 7,
|
||||
FALSE, FALSE,
|
||||
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
|
||||
SERVLIST_X_PADDING, SERVLIST_Y_PADDING);
|
||||
g_signal_connect (G_OBJECT (edit_check_ask_pass), "toggled",
|
||||
G_CALLBACK (servlist_toggle_ask_pass_cb), NULL);
|
||||
|
||||
edit_check_use_keyring = gtk_check_button_new_with_mnemonic (_("Use system keyring"));
|
||||
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (edit_check_use_keyring), net->flags & FLAG_USE_KEYRING);
|
||||
servlist_table_attach (table3, edit_check_use_keyring, 0, 2, 6, 7,
|
||||
servlist_table_attach (table3, edit_check_use_keyring, 0, 2, 7, 8,
|
||||
FALSE, FALSE,
|
||||
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
|
||||
SERVLIST_X_PADDING, SERVLIST_Y_PADDING);
|
||||
g_signal_connect (G_OBJECT (edit_check_use_keyring), "toggled",
|
||||
G_CALLBACK (servlist_toggle_keyring_cb), NULL);
|
||||
|
||||
edit_entry_nick = servlist_create_entry (table3, _("_Nick name:"), 7, net->nick, &edit_label_nick, 0);
|
||||
edit_entry_nick2 = servlist_create_entry (table3, _("Second choice:"), 8, net->nick2, &edit_label_nick2, 0);
|
||||
edit_entry_real = servlist_create_entry (table3, _("Rea_l name:"), 9, net->real, &edit_label_real, 0);
|
||||
edit_entry_user = servlist_create_entry (table3, _("_User name:"), 10, net->user, &edit_label_user, 0);
|
||||
edit_entry_nick = servlist_create_entry (table3, _("_Nick name:"), 8, net->nick, &edit_label_nick, 0);
|
||||
edit_entry_nick2 = servlist_create_entry (table3, _("Second choice:"), 9, net->nick2, &edit_label_nick2, 0);
|
||||
edit_entry_real = servlist_create_entry (table3, _("Rea_l name:"), 10, net->real, &edit_label_real, 0);
|
||||
edit_entry_user = servlist_create_entry (table3, _("_User name:"), 11, net->user, &edit_label_user, 0);
|
||||
|
||||
label_logintype = gtk_label_new (_("Login method:"));
|
||||
servlist_table_attach (table3, label_logintype, 0, 1, 11, 12,
|
||||
servlist_table_attach (table3, label_logintype, 0, 1, 12, 13,
|
||||
FALSE, FALSE,
|
||||
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
|
||||
SERVLIST_X_PADDING, SERVLIST_Y_PADDING);
|
||||
gtk_widget_set_halign (label_logintype, GTK_ALIGN_START);
|
||||
gtk_widget_set_valign (label_logintype, GTK_ALIGN_CENTER);
|
||||
combobox_logintypes = servlist_create_logintypecombo (notebook);
|
||||
servlist_table_attach (table3, combobox_logintypes, 1, 2, 11, 12,
|
||||
servlist_table_attach (table3, combobox_logintypes, 1, 2, 12, 13,
|
||||
FALSE, FALSE,
|
||||
SERVLIST_ALIGN_FILL, SERVLIST_ALIGN_FILL,
|
||||
4, 2);
|
||||
|
||||
edit_entry_pass = servlist_create_entry (table3, _("Password:"), 12, NULL, 0, _("Password used for login. If in doubt, leave blank."));
|
||||
edit_entry_pass = servlist_create_entry (table3, _("Password:"), 13, NULL, 0, _("Password used for login. If in doubt, leave blank."));
|
||||
if (edit_loaded_password)
|
||||
{
|
||||
memset (edit_loaded_password, 0, strlen (edit_loaded_password));
|
||||
@@ -2644,7 +2686,7 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
|
||||
if (selected_net && selected_net->logintype == LOGIN_SASLEXTERNAL)
|
||||
gtk_widget_set_sensitive (edit_entry_pass, FALSE);
|
||||
edit_check_show_pass = gtk_check_button_new_with_mnemonic (_("Show password"));
|
||||
servlist_table_attach (table3, edit_check_show_pass, 0, 2, 13, 14,
|
||||
servlist_table_attach (table3, edit_check_show_pass, 0, 2, 14, 15,
|
||||
FALSE, FALSE,
|
||||
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
|
||||
4, 2);
|
||||
@@ -2652,14 +2694,14 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
|
||||
G_CALLBACK (servlist_toggle_show_password_cb), edit_entry_pass);
|
||||
|
||||
edit_button_encrypt_pass = gtk_button_new_with_mnemonic (_("Encrypt saved password"));
|
||||
servlist_table_attach (table3, edit_button_encrypt_pass, 0, 1, 14, 15,
|
||||
servlist_table_attach (table3, edit_button_encrypt_pass, 0, 1, 15, 16,
|
||||
FALSE, FALSE,
|
||||
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
|
||||
SERVLIST_X_PADDING, SERVLIST_Y_PADDING);
|
||||
g_signal_connect (G_OBJECT (edit_button_encrypt_pass), "clicked",
|
||||
G_CALLBACK (servlist_encrypt_password_cb), net);
|
||||
edit_button_import_pass = gtk_button_new_with_mnemonic (_("Move password to keyring"));
|
||||
servlist_table_attach (table3, edit_button_import_pass, 1, 2, 14, 15,
|
||||
servlist_table_attach (table3, edit_button_import_pass, 1, 2, 15, 16,
|
||||
FALSE, FALSE,
|
||||
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
|
||||
4, 2);
|
||||
@@ -2667,14 +2709,14 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
|
||||
G_CALLBACK (servlist_import_password_cb), net);
|
||||
|
||||
label34 = gtk_label_new (_("Character set:"));
|
||||
servlist_table_attach (table3, label34, 0, 1, 15, 16,
|
||||
servlist_table_attach (table3, label34, 0, 1, 16, 17,
|
||||
FALSE, FALSE,
|
||||
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
|
||||
SERVLIST_X_PADDING, SERVLIST_Y_PADDING);
|
||||
gtk_widget_set_halign (label34, GTK_ALIGN_START);
|
||||
gtk_widget_set_valign (label34, GTK_ALIGN_CENTER);
|
||||
comboboxentry_charset = servlist_create_charsetcombo ();
|
||||
servlist_table_attach (table3, comboboxentry_charset, 1, 2, 15, 16,
|
||||
servlist_table_attach (table3, comboboxentry_charset, 1, 2, 16, 17,
|
||||
FALSE, FALSE,
|
||||
SERVLIST_ALIGN_FILL, SERVLIST_ALIGN_FILL,
|
||||
4, 2);
|
||||
@@ -2700,7 +2742,7 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
|
||||
G_CALLBACK (servlist_delete_client_cert_cb), net);
|
||||
gtk_box_pack_start (GTK_BOX (hbox_cert_buttons), edit_button_cert_delete, FALSE, FALSE, 0);
|
||||
|
||||
servlist_table_attach (table3, hbox_cert_buttons, 0, 2, 16, 17,
|
||||
servlist_table_attach (table3, hbox_cert_buttons, 0, 2, 17, 18,
|
||||
FALSE, FALSE,
|
||||
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
|
||||
SERVLIST_X_PADDING, SERVLIST_Y_PADDING);
|
||||
|
||||
@@ -58,6 +58,7 @@ static gboolean color_change;
|
||||
static struct zoitechatprefs setup_prefs;
|
||||
static GtkWidget *cancel_button;
|
||||
static GtkWidget *font_dialog = NULL;
|
||||
static GtkWidget *setup_topicbar_multiline_toggle = NULL;
|
||||
|
||||
enum
|
||||
{
|
||||
@@ -185,6 +186,10 @@ static const setting appearance_settings[] =
|
||||
{ST_TOGGLR, N_("Show number of users"), P_OFFINTNL(hex_gui_win_ucount),0,0,0},
|
||||
{ST_TOGGLE, N_("Show nickname"), P_OFFINTNL(hex_gui_win_nick),0,0,0},
|
||||
|
||||
{ST_HEADER, N_("Topic Bar"),0,0,0},
|
||||
{ST_TOGGLE, N_("Place mode buttons beside the topic"), P_OFFINTNL(hex_gui_mode_buttons_inline), 0, 0, 0},
|
||||
{ST_TOGGLE, N_("Allow multi-line topics"), P_OFFINTNL(hex_gui_topicbar_multiline), 0, 0, 0},
|
||||
|
||||
{ST_END, 0, 0, 0, 0, 0}
|
||||
};
|
||||
|
||||
@@ -546,6 +551,7 @@ static const setting general_settings[] =
|
||||
{ST_TOGGLE, N_("WHOIS on notify"), P_OFFINTNL(hex_notify_whois_online), N_("Sends a /WHOIS when a user comes online in your notify list."), 0, 0},
|
||||
{ST_TOGGLE, N_("Hide join and part messages"), P_OFFINTNL(hex_irc_conf_mode), N_("Hide channel join/part messages by default."), 0, 0},
|
||||
{ST_TOGGLE, N_("Hide nick change messages"), P_OFFINTNL(hex_irc_hide_nickchange), 0, 0, 0},
|
||||
{ST_TOGGLE, N_("Hide hostmasks in join and part messages"), P_OFFINTNL(hex_irc_hide_join_part_hostmask), 0, 0, 0},
|
||||
{ST_TOGGLE, N_("Enable Ctrl+Q to quit"), P_OFFINTNL(hex_gui_ctrlq_quit), 0, 0, 0},
|
||||
|
||||
{ST_END, 0, 0, 0, 0, 0}
|
||||
@@ -832,6 +838,16 @@ setup_toggle_sensitive_cb (GtkToggleButton *but, GtkWidget *wid)
|
||||
gtk_widget_set_sensitive (wid, gtk_toggle_button_get_active (but));
|
||||
}
|
||||
|
||||
static void
|
||||
setup_topicbar_inline_toggled_cb (GtkToggleButton *but, gpointer userdata)
|
||||
{
|
||||
(void) userdata;
|
||||
|
||||
if (setup_topicbar_multiline_toggle)
|
||||
gtk_widget_set_sensitive (setup_topicbar_multiline_toggle,
|
||||
!gtk_toggle_button_get_active (but));
|
||||
}
|
||||
|
||||
static void
|
||||
setup_create_toggleR (GtkWidget *tab, int row, const setting *set)
|
||||
{
|
||||
@@ -858,6 +874,14 @@ setup_create_toggleL (GtkWidget *tab, int row, const setting *set)
|
||||
setup_get_int (&setup_prefs, set));
|
||||
g_signal_connect (G_OBJECT (wid), "toggled",
|
||||
G_CALLBACK (setup_toggle_cb), (gpointer)set);
|
||||
if (set->offset == STRUCT_OFFSET_INT (struct zoitechatprefs, hex_gui_mode_buttons_inline))
|
||||
g_signal_connect (G_OBJECT (wid), "toggled",
|
||||
G_CALLBACK (setup_topicbar_inline_toggled_cb), NULL);
|
||||
if (set->offset == STRUCT_OFFSET_INT (struct zoitechatprefs, hex_gui_topicbar_multiline))
|
||||
{
|
||||
setup_topicbar_multiline_toggle = wid;
|
||||
gtk_widget_set_sensitive (wid, !setup_prefs.hex_gui_mode_buttons_inline);
|
||||
}
|
||||
if (set->tooltip)
|
||||
gtk_widget_set_tooltip_text (wid, _(set->tooltip));
|
||||
setup_table_attach (tab, wid, 2, row==6 ? 6 : 4, row, row + 1, FALSE, FALSE,
|
||||
@@ -2285,6 +2309,8 @@ setup_apply (struct zoitechatprefs *pr)
|
||||
noapply = TRUE;
|
||||
if (DIFF (hex_gui_lagometer))
|
||||
noapply = TRUE;
|
||||
if (DIFF (hex_gui_mode_buttons_inline))
|
||||
noapply = TRUE;
|
||||
if (DIFF (hex_gui_tab_icons))
|
||||
noapply = TRUE;
|
||||
if (DIFF (hex_gui_tab_closebuttons))
|
||||
@@ -2299,6 +2325,8 @@ setup_apply (struct zoitechatprefs *pr)
|
||||
noapply = TRUE;
|
||||
if (DIFF (hex_gui_throttlemeter))
|
||||
noapply = TRUE;
|
||||
if (DIFF (hex_gui_topicbar_multiline))
|
||||
noapply = TRUE;
|
||||
if (DIFF (hex_gui_ulist_count))
|
||||
noapply = TRUE;
|
||||
if (DIFF (hex_gui_ulist_icons))
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <math.h>
|
||||
|
||||
#include "../theme-access.h"
|
||||
#include "../theme-css.h"
|
||||
#include "../theme-manager.h"
|
||||
#include "../theme-runtime.h"
|
||||
#include "../../xtext-color.h"
|
||||
@@ -54,6 +55,13 @@ setup_apply_real (const ThemeChangedEvent *event)
|
||||
(void) event;
|
||||
}
|
||||
|
||||
void
|
||||
gtkutil_append_font_css (GString *css, const PangoFontDescription *font_desc)
|
||||
{
|
||||
(void) css;
|
||||
(void) font_desc;
|
||||
}
|
||||
|
||||
gboolean
|
||||
fe_dark_mode_is_enabled_for (unsigned int mode)
|
||||
{
|
||||
@@ -173,6 +181,14 @@ theme_gtk3_is_active (void)
|
||||
return stub_gtk3_active;
|
||||
}
|
||||
|
||||
gboolean
|
||||
theme_gtk3_lookup_theme_color (const char *name, GdkRGBA *out_color)
|
||||
{
|
||||
(void) name;
|
||||
(void) out_color;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
rgba_equal (const GdkRGBA *a, const GdkRGBA *b)
|
||||
{
|
||||
@@ -336,6 +352,79 @@ test_access_xtext_palette_widget_mapping_when_gtk3_active (void)
|
||||
g_object_unref (provider);
|
||||
}
|
||||
|
||||
static void
|
||||
test_access_palette_css_does_not_feed_back_into_sampling (void)
|
||||
{
|
||||
GtkWidget *window;
|
||||
GtkWidget *label;
|
||||
GtkCssProvider *theme1;
|
||||
GtkCssProvider *theme2;
|
||||
GdkScreen *screen;
|
||||
XTextColor palette[2] = { 0 };
|
||||
GdkRGBA old_bg;
|
||||
GdkRGBA old_fg;
|
||||
GdkRGBA expected;
|
||||
|
||||
if (!gtk_available)
|
||||
{
|
||||
g_test_message ("GTK display not available");
|
||||
return;
|
||||
}
|
||||
|
||||
reset_stubs ();
|
||||
stub_gtk3_active = TRUE;
|
||||
screen = gdk_screen_get_default ();
|
||||
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
|
||||
label = gtk_label_new ("mapped");
|
||||
gtk_container_add (GTK_CONTAINER (window), label);
|
||||
gtk_widget_realize (window);
|
||||
|
||||
/* Old GTK3 theme active at screen level. */
|
||||
theme1 = gtk_css_provider_new ();
|
||||
gtk_css_provider_load_from_data (theme1,
|
||||
"label { color: #112233; background-color: #445566; }"
|
||||
"label:selected { color: #778899; background-color: #aabbcc; }",
|
||||
-1, NULL);
|
||||
gtk_style_context_add_provider_for_screen (screen,
|
||||
GTK_STYLE_PROVIDER (theme1), GTK_STYLE_PROVIDER_PRIORITY_USER);
|
||||
|
||||
/* The app paints the sampled theme colors onto the widget, like the
|
||||
* user list / channel tree / input box palette provider does. */
|
||||
g_assert_true (gdk_rgba_parse (&old_bg, "#445566"));
|
||||
g_assert_true (gdk_rgba_parse (&old_fg, "#112233"));
|
||||
theme_css_apply_palette_widget (label, &old_bg, &old_fg, NULL);
|
||||
|
||||
/* The user switches to a new GTK3 theme. */
|
||||
gtk_style_context_remove_provider_for_screen (screen, GTK_STYLE_PROVIDER (theme1));
|
||||
theme2 = gtk_css_provider_new ();
|
||||
gtk_css_provider_load_from_data (theme2,
|
||||
"label { color: #00ff00; background-color: #ff0000; }"
|
||||
"label:selected { color: #0000ff; background-color: #ffff00; }",
|
||||
-1, NULL);
|
||||
gtk_style_context_add_provider_for_screen (screen,
|
||||
GTK_STYLE_PROVIDER (theme2), GTK_STYLE_PROVIDER_PRIORITY_USER);
|
||||
gtk_style_context_reset_widgets (screen);
|
||||
|
||||
/* Re-sampling must see the new theme, not the palette colors the
|
||||
* app applied for the old theme. */
|
||||
theme_get_xtext_colors_for_widget (label, palette, G_N_ELEMENTS (palette));
|
||||
|
||||
g_assert_true (stub_last_gtk_map_valid);
|
||||
g_assert_true (gdk_rgba_parse (&expected, "#00ff00"));
|
||||
g_assert_true (rgba_close (&stub_last_gtk_map.text_foreground, &expected));
|
||||
g_assert_true (gdk_rgba_parse (&expected, "#ff0000"));
|
||||
g_assert_true (rgba_close (&stub_last_gtk_map.text_background, &expected));
|
||||
|
||||
/* Sampling must not permanently strip the palette class. */
|
||||
g_assert_true (gtk_style_context_has_class (gtk_widget_get_style_context (label),
|
||||
theme_css_palette_class_name ()));
|
||||
|
||||
gtk_style_context_remove_provider_for_screen (screen, GTK_STYLE_PROVIDER (theme2));
|
||||
gtk_widget_destroy (window);
|
||||
g_object_unref (theme1);
|
||||
g_object_unref (theme2);
|
||||
}
|
||||
|
||||
static void
|
||||
test_access_dark_light_switch_affects_token_consumers (void)
|
||||
{
|
||||
@@ -363,6 +452,8 @@ main (int argc, char **argv)
|
||||
g_test_add_func ("/theme/access/xtext_palette_forwarding", test_access_xtext_palette_forwarding);
|
||||
g_test_add_func ("/theme/access/xtext_palette_widget_mapping_when_gtk3_active",
|
||||
test_access_xtext_palette_widget_mapping_when_gtk3_active);
|
||||
g_test_add_func ("/theme/access/palette_css_does_not_feed_back_into_sampling",
|
||||
test_access_palette_css_does_not_feed_back_into_sampling);
|
||||
g_test_add_func ("/theme/access/widget_style_forwarding", test_access_widget_style_forwarding);
|
||||
g_test_add_func ("/theme/access/dark_light_switch_affects_token_consumers",
|
||||
test_access_dark_light_switch_affects_token_consumers);
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "../../fe-gtk.h"
|
||||
|
||||
#include "../theme-application.h"
|
||||
#include "../theme-policy.h"
|
||||
#include "../../maingui.h"
|
||||
#include "../../../common/zoitechat.h"
|
||||
#include "../../../common/zoitechatc.h"
|
||||
@@ -54,6 +55,19 @@ theme_css_reload_input_style (gboolean enabled, const PangoFontDescription *font
|
||||
css_reload_calls++;
|
||||
}
|
||||
|
||||
gboolean
|
||||
theme_policy_system_prefers_dark (void)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
gboolean
|
||||
theme_policy_is_dark_mode_active (unsigned int mode)
|
||||
{
|
||||
(void) mode;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
void
|
||||
theme_runtime_load (void)
|
||||
{
|
||||
@@ -104,7 +118,9 @@ test_invalid_font_falls_back_to_sans_11 (void)
|
||||
g_assert_cmpint (pango_font_description_get_size (style->font_desc), ==, 11 * PANGO_SCALE);
|
||||
g_assert_cmpint (message_calls, ==, 1);
|
||||
g_assert_cmpint (css_reload_calls, ==, 1);
|
||||
g_assert_true (css_enabled);
|
||||
/* Input boxes are themed per-widget; the app-global input CSS stays
|
||||
* off so entries render natively under the active GTK3 theme. */
|
||||
g_assert_false (css_enabled);
|
||||
g_assert_nonnull (css_font_desc);
|
||||
|
||||
if (style->font_desc)
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <glib.h>
|
||||
#include <glib/gstdio.h>
|
||||
#include <gtk/gtk.h>
|
||||
#include <gio/gio.h>
|
||||
|
||||
#include "../theme-gtk3.h"
|
||||
#include "../../../common/gtk3-theme-service.h"
|
||||
@@ -35,14 +36,18 @@ struct zoitechatprefs prefs;
|
||||
|
||||
static gboolean gtk_available;
|
||||
static char *temp_root;
|
||||
static char *xdg_data_home;
|
||||
static char *theme_parent_root;
|
||||
static char *theme_child_root;
|
||||
static char *theme_switch_root;
|
||||
static char *theme_chrome_root;
|
||||
|
||||
static gboolean stub_system_prefers_dark;
|
||||
|
||||
gboolean
|
||||
theme_policy_system_prefers_dark (void)
|
||||
{
|
||||
return FALSE;
|
||||
return stub_system_prefers_dark;
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -130,6 +135,8 @@ zoitechat_gtk3_theme_find_by_id (const char *theme_id)
|
||||
return make_theme (theme_id, theme_child_root);
|
||||
if (g_strcmp0 (theme_id, "switch") == 0)
|
||||
return make_theme (theme_id, theme_switch_root);
|
||||
if (g_strcmp0 (theme_id, "chrome") == 0)
|
||||
return make_theme (theme_id, theme_chrome_root);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -176,6 +183,11 @@ zoitechat_gtk3_theme_build_inheritance_chain (const char *theme_root)
|
||||
g_ptr_array_add (chain, g_strdup (theme_switch_root));
|
||||
return chain;
|
||||
}
|
||||
if (g_strcmp0 (theme_root, theme_chrome_root) == 0)
|
||||
{
|
||||
g_ptr_array_add (chain, g_strdup (theme_chrome_root));
|
||||
return chain;
|
||||
}
|
||||
g_ptr_array_unref (chain);
|
||||
return NULL;
|
||||
}
|
||||
@@ -198,6 +210,14 @@ get_int_setting (const char *name)
|
||||
return value;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
has_default_seat (void)
|
||||
{
|
||||
GdkDisplay *display = gdk_display_get_default ();
|
||||
|
||||
return display && GDK_IS_SEAT (gdk_display_get_default_seat (display));
|
||||
}
|
||||
|
||||
static void
|
||||
setup_themes (void)
|
||||
{
|
||||
@@ -205,6 +225,10 @@ setup_themes (void)
|
||||
|
||||
temp_root = g_dir_make_tmp ("zoitechat-theme-gtk3-settings-XXXXXX", NULL);
|
||||
g_assert_nonnull (temp_root);
|
||||
xdg_data_home = g_build_filename (temp_root, "data", NULL);
|
||||
g_assert_cmpint (g_mkdir_with_parents (xdg_data_home, 0700), ==, 0);
|
||||
g_setenv ("XDG_DATA_HOME", xdg_data_home, TRUE);
|
||||
|
||||
theme_parent_root = g_build_filename (temp_root, "parent", NULL);
|
||||
theme_child_root = g_build_filename (temp_root, "child", NULL);
|
||||
theme_switch_root = g_build_filename (temp_root, "switch", NULL);
|
||||
@@ -234,6 +258,22 @@ setup_themes (void)
|
||||
"gtk-enable-animations=false\n"
|
||||
"gtk-cursor-blink-time=444\n");
|
||||
|
||||
theme_chrome_root = g_build_filename (temp_root, "chrome", NULL);
|
||||
g_assert_cmpint (g_mkdir_with_parents (theme_chrome_root, 0700), ==, 0);
|
||||
ensure_css_dir (theme_chrome_root, "gtk-3.24");
|
||||
/* Defines named colors but no window/menubar/menu rules of its own,
|
||||
* so all chrome styling must come from the backfill provider. */
|
||||
{
|
||||
char *css = g_build_filename (theme_chrome_root, "gtk-3.24", "gtk.css", NULL);
|
||||
write_file (css,
|
||||
"@define-color theme_bg_color #e8f4e8;\n"
|
||||
"@define-color theme_fg_color #102010;\n"
|
||||
"@define-color theme_selected_bg_color #3465a4;\n"
|
||||
"@define-color theme_selected_fg_color #ffffff;\n"
|
||||
"label { color: #102010; }\n");
|
||||
g_free (css);
|
||||
}
|
||||
|
||||
path = g_build_filename (theme_parent_root, "index.theme", NULL);
|
||||
write_file (path, "[Desktop Entry]\nName=parent\n");
|
||||
g_free (path);
|
||||
@@ -253,10 +293,14 @@ teardown_themes (void)
|
||||
g_free (theme_parent_root);
|
||||
g_free (theme_child_root);
|
||||
g_free (theme_switch_root);
|
||||
g_free (theme_chrome_root);
|
||||
g_free (xdg_data_home);
|
||||
g_free (temp_root);
|
||||
theme_parent_root = NULL;
|
||||
theme_child_root = NULL;
|
||||
theme_switch_root = NULL;
|
||||
theme_chrome_root = NULL;
|
||||
xdg_data_home = NULL;
|
||||
temp_root = NULL;
|
||||
}
|
||||
|
||||
@@ -302,10 +346,13 @@ test_settings_restored_on_disable_and_switch (void)
|
||||
g_assert_true (theme_gtk3_apply ("layered", THEME_GTK3_VARIANT_PREFER_LIGHT, &error));
|
||||
g_assert_no_error (error);
|
||||
g_assert_cmpint (get_int_setting ("gtk-cursor-blink-time"), ==, 333);
|
||||
g_object_get (gtk_settings_get_default (), "gtk-theme-name", &active_theme_name, NULL);
|
||||
g_assert_cmpstr (active_theme_name, ==, "child");
|
||||
g_free (active_theme_name);
|
||||
active_theme_name = NULL;
|
||||
if (has_default_seat ())
|
||||
{
|
||||
g_object_get (gtk_settings_get_default (), "gtk-theme-name", &active_theme_name, NULL);
|
||||
g_assert_cmpstr (active_theme_name, ==, "child");
|
||||
g_free (active_theme_name);
|
||||
active_theme_name = NULL;
|
||||
}
|
||||
|
||||
g_assert_true (theme_gtk3_apply ("switch", THEME_GTK3_VARIANT_PREFER_LIGHT, &error));
|
||||
g_assert_no_error (error);
|
||||
@@ -315,24 +362,234 @@ test_settings_restored_on_disable_and_switch (void)
|
||||
theme_gtk3_disable ();
|
||||
g_assert_cmpint (get_int_setting ("gtk-cursor-blink-time"), ==, default_blink);
|
||||
g_assert_cmpint (get_bool_setting ("gtk-enable-animations"), ==, default_animations);
|
||||
g_object_get (gtk_settings_get_default (), "gtk-theme-name", &active_theme_name, NULL);
|
||||
g_assert_cmpstr (active_theme_name, ==, default_theme_name);
|
||||
g_free (active_theme_name);
|
||||
if (has_default_seat ())
|
||||
{
|
||||
g_object_get (gtk_settings_get_default (), "gtk-theme-name", &active_theme_name, NULL);
|
||||
g_assert_cmpstr (active_theme_name, ==, default_theme_name);
|
||||
g_free (active_theme_name);
|
||||
}
|
||||
g_free (default_theme_name);
|
||||
g_assert_false (theme_gtk3_is_active ());
|
||||
}
|
||||
|
||||
static void
|
||||
test_follow_system_variant_resolves_to_theme_variant (void)
|
||||
{
|
||||
GError *error = NULL;
|
||||
|
||||
if (!gtk_available)
|
||||
{
|
||||
g_test_message ("GTK display not available");
|
||||
return;
|
||||
}
|
||||
|
||||
/* An explicitly selected in-app theme must not follow the OS
|
||||
* light/dark preference: the legacy FOLLOW_SYSTEM variant resolves
|
||||
* to the variant the theme itself provides, so the window chrome
|
||||
* (menu bar) stays on the theme even when the system prefers dark. */
|
||||
stub_system_prefers_dark = TRUE;
|
||||
g_assert_true (theme_gtk3_apply ("switch", THEME_GTK3_VARIANT_FOLLOW_SYSTEM, &error));
|
||||
g_assert_no_error (error);
|
||||
g_assert_true (theme_gtk3_is_active ());
|
||||
g_assert_cmpint (theme_gtk3_active_variant (), ==, THEME_GTK3_VARIANT_PREFER_LIGHT);
|
||||
g_assert_false (get_bool_setting ("gtk-application-prefer-dark-theme"));
|
||||
|
||||
stub_system_prefers_dark = FALSE;
|
||||
theme_gtk3_disable ();
|
||||
}
|
||||
|
||||
static void
|
||||
test_menu_bar_follows_active_theme_colors (void)
|
||||
{
|
||||
GError *error = NULL;
|
||||
GtkWidget *window;
|
||||
GtkWidget *menubar;
|
||||
GtkStyleContext *context;
|
||||
GdkRGBA bg;
|
||||
GdkRGBA expected;
|
||||
|
||||
if (!gtk_available)
|
||||
{
|
||||
g_test_message ("GTK display not available");
|
||||
return;
|
||||
}
|
||||
|
||||
/* The chrome theme defines no menubar rules; without the backfill
|
||||
* provider the menu bar would fall through to the base (OS) theme
|
||||
* and follow the OS dark/light mode instead of the selected theme. */
|
||||
g_assert_true (theme_gtk3_apply ("chrome", THEME_GTK3_VARIANT_PREFER_LIGHT, &error));
|
||||
g_assert_no_error (error);
|
||||
|
||||
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
|
||||
menubar = gtk_menu_bar_new ();
|
||||
gtk_container_add (GTK_CONTAINER (window), menubar);
|
||||
gtk_widget_show_all (window);
|
||||
|
||||
context = gtk_widget_get_style_context (menubar);
|
||||
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
|
||||
gtk_style_context_get_background_color (context, GTK_STATE_FLAG_NORMAL, &bg);
|
||||
G_GNUC_END_IGNORE_DEPRECATIONS
|
||||
g_assert_true (gdk_rgba_parse (&expected, "#e8f4e8"));
|
||||
g_assert_cmpfloat (ABS (bg.red - expected.red), <, 0.01);
|
||||
g_assert_cmpfloat (ABS (bg.green - expected.green), <, 0.01);
|
||||
g_assert_cmpfloat (ABS (bg.blue - expected.blue), <, 0.01);
|
||||
|
||||
/* The window background must follow the theme too, or the space
|
||||
* around and beside the menu bar keeps the OS theme's colors. */
|
||||
context = gtk_widget_get_style_context (window);
|
||||
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
|
||||
gtk_style_context_get_background_color (context, GTK_STATE_FLAG_NORMAL, &bg);
|
||||
G_GNUC_END_IGNORE_DEPRECATIONS
|
||||
g_assert_cmpfloat (ABS (bg.red - expected.red), <, 0.01);
|
||||
g_assert_cmpfloat (ABS (bg.green - expected.green), <, 0.01);
|
||||
g_assert_cmpfloat (ABS (bg.blue - expected.blue), <, 0.01);
|
||||
|
||||
gtk_widget_destroy (window);
|
||||
theme_gtk3_disable ();
|
||||
}
|
||||
|
||||
static void
|
||||
test_desktop_color_sync_does_not_leak_into_chrome (void)
|
||||
{
|
||||
GError *error = NULL;
|
||||
GtkWidget *window;
|
||||
GtkWidget *menubar;
|
||||
GtkStyleContext *context;
|
||||
GtkCssProvider *desktop_css;
|
||||
GdkScreen *screen;
|
||||
GdkRGBA bg;
|
||||
GdkRGBA expected;
|
||||
GdkRGBA injected;
|
||||
|
||||
if (!gtk_available)
|
||||
{
|
||||
g_test_message ("GTK display not available");
|
||||
return;
|
||||
}
|
||||
|
||||
/* KDE's GTK color-scheme sync (~/.config/gtk-3.0/gtk.css) redefines
|
||||
* the standard named colors with the OS palette at USER priority.
|
||||
* The switch theme defines no named colors of its own, so a naive
|
||||
* @theme_bg_color reference in the chrome backfill would resolve to
|
||||
* the injected OS dark palette; the backfill must use its neutral
|
||||
* variant fallback instead. */
|
||||
screen = gdk_screen_get_default ();
|
||||
desktop_css = gtk_css_provider_new ();
|
||||
gtk_css_provider_load_from_data (desktop_css,
|
||||
"@define-color theme_bg_color #1e1e1e;\n"
|
||||
"@define-color theme_fg_color #eeeeee;\n"
|
||||
"menubar { background-color: #1e1e1e; }\n",
|
||||
-1, NULL);
|
||||
gtk_style_context_add_provider_for_screen (screen,
|
||||
GTK_STYLE_PROVIDER (desktop_css), GTK_STYLE_PROVIDER_PRIORITY_USER);
|
||||
|
||||
g_assert_true (theme_gtk3_apply ("switch", THEME_GTK3_VARIANT_PREFER_LIGHT, &error));
|
||||
g_assert_no_error (error);
|
||||
|
||||
{
|
||||
GtkWidget *item;
|
||||
GdkRGBA item_bg;
|
||||
|
||||
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
|
||||
menubar = gtk_menu_bar_new ();
|
||||
item = gtk_menu_item_new_with_label ("File");
|
||||
gtk_menu_shell_append (GTK_MENU_SHELL (menubar), item);
|
||||
gtk_container_add (GTK_CONTAINER (window), menubar);
|
||||
gtk_widget_show_all (window);
|
||||
|
||||
context = gtk_widget_get_style_context (menubar);
|
||||
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
|
||||
gtk_style_context_get_background_color (context, GTK_STATE_FLAG_NORMAL, &bg);
|
||||
G_GNUC_END_IGNORE_DEPRECATIONS
|
||||
g_assert_true (gdk_rgba_parse (&expected, "#f6f5f4"));
|
||||
g_assert_true (gdk_rgba_parse (&injected, "#1e1e1e"));
|
||||
g_assert_cmpfloat (ABS (bg.red - injected.red), >, 0.1);
|
||||
g_assert_cmpfloat (ABS (bg.red - expected.red), <, 0.01);
|
||||
g_assert_cmpfloat (ABS (bg.green - expected.green), <, 0.01);
|
||||
g_assert_cmpfloat (ABS (bg.blue - expected.blue), <, 0.01);
|
||||
|
||||
/* The menu bar body and the menu items must be one surface. */
|
||||
context = gtk_widget_get_style_context (item);
|
||||
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
|
||||
gtk_style_context_get_background_color (context, GTK_STATE_FLAG_NORMAL, &item_bg);
|
||||
G_GNUC_END_IGNORE_DEPRECATIONS
|
||||
g_assert_cmpfloat (ABS (item_bg.red - bg.red), <, 0.01);
|
||||
g_assert_cmpfloat (ABS (item_bg.green - bg.green), <, 0.01);
|
||||
g_assert_cmpfloat (ABS (item_bg.blue - bg.blue), <, 0.01);
|
||||
}
|
||||
|
||||
gtk_widget_destroy (window);
|
||||
theme_gtk3_disable ();
|
||||
gtk_style_context_remove_provider_for_screen (screen, GTK_STYLE_PROVIDER (desktop_css));
|
||||
g_object_unref (desktop_css);
|
||||
}
|
||||
|
||||
static void
|
||||
test_stale_theme_symlink_is_replaced (void)
|
||||
{
|
||||
GError *error = NULL;
|
||||
char *themes_dir;
|
||||
char *link_path;
|
||||
char *target;
|
||||
char *active_theme_name = NULL;
|
||||
|
||||
if (!gtk_available)
|
||||
{
|
||||
g_test_message ("GTK display not available");
|
||||
return;
|
||||
}
|
||||
if (!has_default_seat ())
|
||||
{
|
||||
g_test_message ("No default seat available");
|
||||
return;
|
||||
}
|
||||
|
||||
/* A dangling symlink left behind by a removed theme must not block
|
||||
* pinning gtk-theme-name to the newly applied theme. */
|
||||
themes_dir = g_build_filename (xdg_data_home, "themes", NULL);
|
||||
g_assert_cmpint (g_mkdir_with_parents (themes_dir, 0700), ==, 0);
|
||||
link_path = g_build_filename (themes_dir, "switch", NULL);
|
||||
g_unlink (link_path);
|
||||
{
|
||||
GFile *link_file = g_file_new_for_path (link_path);
|
||||
g_assert_true (g_file_make_symbolic_link (link_file, "/nonexistent/switch", NULL, NULL));
|
||||
g_object_unref (link_file);
|
||||
}
|
||||
|
||||
g_assert_true (theme_gtk3_apply ("switch", THEME_GTK3_VARIANT_PREFER_LIGHT, &error));
|
||||
g_assert_no_error (error);
|
||||
|
||||
target = g_file_read_link (link_path, NULL);
|
||||
g_assert_cmpstr (target, ==, theme_switch_root);
|
||||
g_object_get (gtk_settings_get_default (), "gtk-theme-name", &active_theme_name, NULL);
|
||||
g_assert_cmpstr (active_theme_name, ==, "switch");
|
||||
|
||||
g_free (active_theme_name);
|
||||
g_free (target);
|
||||
g_free (link_path);
|
||||
g_free (themes_dir);
|
||||
theme_gtk3_disable ();
|
||||
}
|
||||
|
||||
int
|
||||
main (int argc, char **argv)
|
||||
{
|
||||
int rc;
|
||||
|
||||
g_test_init (&argc, &argv, NULL);
|
||||
gtk_available = gtk_init_check (&argc, &argv);
|
||||
setup_themes ();
|
||||
gtk_available = gtk_init_check (&argc, &argv);
|
||||
|
||||
g_test_add_func ("/theme/gtk3/settings_layer_precedence", test_settings_layer_precedence);
|
||||
g_test_add_func ("/theme/gtk3/settings_restored_on_disable_and_switch", test_settings_restored_on_disable_and_switch);
|
||||
g_test_add_func ("/theme/gtk3/follow_system_variant_resolves_to_theme_variant",
|
||||
test_follow_system_variant_resolves_to_theme_variant);
|
||||
g_test_add_func ("/theme/gtk3/menu_bar_follows_active_theme_colors",
|
||||
test_menu_bar_follows_active_theme_colors);
|
||||
g_test_add_func ("/theme/gtk3/desktop_color_sync_does_not_leak_into_chrome",
|
||||
test_desktop_color_sync_does_not_leak_into_chrome);
|
||||
g_test_add_func ("/theme/gtk3/stale_theme_symlink_is_replaced",
|
||||
test_stale_theme_symlink_is_replaced);
|
||||
|
||||
prefs.hex_gui_gtk3_variant = THEME_GTK3_VARIANT_PREFER_LIGHT;
|
||||
|
||||
|
||||
@@ -89,3 +89,9 @@ theme_gtk3_is_active (void)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
ThemeGtk3Variant
|
||||
theme_gtk3_active_variant (void)
|
||||
{
|
||||
return THEME_GTK3_VARIANT_FOLLOW_SYSTEM;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
#include "../theme-manager.h"
|
||||
#include "../theme-gtk3.h"
|
||||
#include "../theme-policy.h"
|
||||
#include "../../../common/zoitechat.h"
|
||||
#include "../../../common/zoitechatc.h"
|
||||
|
||||
@@ -84,6 +85,15 @@ gboolean theme_policy_system_prefers_dark (void)
|
||||
return stub_system_prefers_dark;
|
||||
}
|
||||
|
||||
void theme_policy_init (void)
|
||||
{
|
||||
}
|
||||
|
||||
void theme_policy_set_appearance_changed_callback (ThemePolicyAppearanceChangedFunc callback)
|
||||
{
|
||||
(void) callback;
|
||||
}
|
||||
|
||||
gboolean theme_application_apply_mode (unsigned int mode, gboolean *palette_changed)
|
||||
{
|
||||
(void) mode;
|
||||
@@ -203,6 +213,7 @@ reset_state (void)
|
||||
idle_add_calls = 0;
|
||||
next_idle_source_id = 33;
|
||||
prefs.hex_gui_gtk3_variant = THEME_GTK3_VARIANT_FOLLOW_SYSTEM;
|
||||
prefs.hex_gui_gtk3_theme[0] = '\0';
|
||||
test_theme_gtk3_stub_reset ();
|
||||
}
|
||||
|
||||
@@ -235,12 +246,14 @@ test_auto_refresh_dispatches_mode_palette_and_style_reasons (void)
|
||||
}
|
||||
|
||||
static void
|
||||
test_auto_refresh_ignores_non_auto_mode (void)
|
||||
test_auto_refresh_ignores_locked_in_app_theme (void)
|
||||
{
|
||||
guint listener_id;
|
||||
|
||||
reset_state ();
|
||||
prefs.hex_gui_dark_mode = ZOITECHAT_DARK_MODE_DARK;
|
||||
g_strlcpy (prefs.hex_gui_gtk3_theme, "locked-theme", sizeof (prefs.hex_gui_gtk3_theme));
|
||||
prefs.hex_gui_gtk3_variant = THEME_GTK3_VARIANT_PREFER_LIGHT;
|
||||
stub_apply_mode_palette_changed = TRUE;
|
||||
listener_id = theme_listener_register ("auto.nonauto", auto_listener, NULL);
|
||||
theme_manager_set_idle_add_func (immediate_idle_add);
|
||||
@@ -254,6 +267,7 @@ test_auto_refresh_ignores_non_auto_mode (void)
|
||||
|
||||
theme_manager_set_idle_add_func (NULL);
|
||||
theme_listener_unregister (listener_id);
|
||||
prefs.hex_gui_gtk3_theme[0] = '\0';
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -263,6 +277,7 @@ test_auto_refresh_reapplies_gtk3_for_follow_system_variant (void)
|
||||
|
||||
reset_state ();
|
||||
prefs.hex_gui_dark_mode = ZOITECHAT_DARK_MODE_DARK;
|
||||
g_strlcpy (prefs.hex_gui_gtk3_theme, "system-follower", sizeof (prefs.hex_gui_gtk3_theme));
|
||||
prefs.hex_gui_gtk3_variant = THEME_GTK3_VARIANT_FOLLOW_SYSTEM;
|
||||
listener_id = theme_listener_register ("auto.gtk3", auto_listener, NULL);
|
||||
theme_manager_set_idle_add_func (immediate_idle_add);
|
||||
@@ -274,6 +289,39 @@ test_auto_refresh_reapplies_gtk3_for_follow_system_variant (void)
|
||||
g_assert_cmpint (listener_calls, ==, 0);
|
||||
g_assert_cmpint (test_theme_gtk3_stub_apply_current_calls (), ==, 1);
|
||||
|
||||
theme_manager_set_idle_add_func (NULL);
|
||||
theme_listener_unregister (listener_id);
|
||||
prefs.hex_gui_gtk3_theme[0] = '\0';
|
||||
}
|
||||
|
||||
static void
|
||||
test_auto_refresh_follows_system_theme_without_auto_mode (void)
|
||||
{
|
||||
guint listener_id;
|
||||
|
||||
reset_state ();
|
||||
prefs.hex_gui_dark_mode = ZOITECHAT_DARK_MODE_DARK;
|
||||
prefs.hex_gui_gtk3_variant = THEME_GTK3_VARIANT_PREFER_LIGHT;
|
||||
listener_id = theme_listener_register ("auto.system", auto_listener, NULL);
|
||||
theme_manager_set_idle_add_func (immediate_idle_add);
|
||||
|
||||
theme_manager_refresh_auto_mode ();
|
||||
|
||||
/* No in-app theme is selected, so a system theme change must
|
||||
* re-resolve the mapped palette even outside auto dark mode. */
|
||||
g_assert_cmpint (idle_add_calls, ==, 1);
|
||||
g_assert_cmpint (auto_state_calls, ==, 0);
|
||||
g_assert_cmpint (listener_calls, ==, 1);
|
||||
g_assert_true (theme_changed_event_has_reason (&last_event, THEME_CHANGED_REASON_PALETTE));
|
||||
g_assert_true (theme_changed_event_has_reason (&last_event, THEME_CHANGED_REASON_WIDGET_STYLE));
|
||||
g_assert_true (theme_changed_event_has_reason (&last_event, THEME_CHANGED_REASON_USERLIST));
|
||||
g_assert_true (theme_changed_event_has_reason (&last_event, THEME_CHANGED_REASON_MODE));
|
||||
|
||||
/* Unchanged system state must not dispatch again. */
|
||||
theme_manager_refresh_auto_mode ();
|
||||
g_assert_cmpint (idle_add_calls, ==, 2);
|
||||
g_assert_cmpint (listener_calls, ==, 1);
|
||||
|
||||
theme_manager_set_idle_add_func (NULL);
|
||||
theme_listener_unregister (listener_id);
|
||||
}
|
||||
@@ -284,9 +332,11 @@ main (int argc, char **argv)
|
||||
g_test_init (&argc, &argv, NULL);
|
||||
g_test_add_func ("/theme/manager/auto_refresh_dispatches_mode_palette_and_style_reasons",
|
||||
test_auto_refresh_dispatches_mode_palette_and_style_reasons);
|
||||
g_test_add_func ("/theme/manager/auto_refresh_ignores_non_auto_mode",
|
||||
test_auto_refresh_ignores_non_auto_mode);
|
||||
g_test_add_func ("/theme/manager/auto_refresh_ignores_locked_in_app_theme",
|
||||
test_auto_refresh_ignores_locked_in_app_theme);
|
||||
g_test_add_func ("/theme/manager/auto_refresh_reapplies_gtk3_for_follow_system_variant",
|
||||
test_auto_refresh_reapplies_gtk3_for_follow_system_variant);
|
||||
g_test_add_func ("/theme/manager/auto_refresh_follows_system_theme_without_auto_mode",
|
||||
test_auto_refresh_follows_system_theme_without_auto_mode);
|
||||
return g_test_run ();
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "../theme-manager.h"
|
||||
#include "../theme-policy.h"
|
||||
#include "../../../common/zoitechat.h"
|
||||
#include "../../../common/zoitechatc.h"
|
||||
|
||||
@@ -77,6 +78,15 @@ gboolean theme_policy_system_prefers_dark (void)
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
void theme_policy_init (void)
|
||||
{
|
||||
}
|
||||
|
||||
void theme_policy_set_appearance_changed_callback (ThemePolicyAppearanceChangedFunc callback)
|
||||
{
|
||||
(void) callback;
|
||||
}
|
||||
|
||||
gboolean theme_application_apply_mode (unsigned int mode, gboolean *palette_changed)
|
||||
{
|
||||
(void) mode;
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
#include "../theme-palette.h"
|
||||
#include "../theme-manager.h"
|
||||
#include "../theme-policy.h"
|
||||
#include "../../../common/zoitechat.h"
|
||||
#include "../../../common/zoitechatc.h"
|
||||
|
||||
@@ -30,6 +31,7 @@ struct session *current_tab;
|
||||
struct session *lastact_sess;
|
||||
struct zoitechatprefs prefs;
|
||||
|
||||
static gboolean gtk_available;
|
||||
static gboolean stub_policy_dark;
|
||||
static unsigned int stub_policy_mode;
|
||||
static gboolean stub_apply_mode_result;
|
||||
@@ -79,7 +81,18 @@ void zoitechat_set_theme_post_apply_callback (zoitechat_theme_post_apply_callbac
|
||||
gboolean theme_policy_is_dark_mode_active (unsigned int mode)
|
||||
{
|
||||
stub_policy_mode = mode;
|
||||
return stub_policy_dark;
|
||||
if (mode == ZOITECHAT_DARK_MODE_AUTO)
|
||||
return stub_policy_dark;
|
||||
return fe_dark_mode_is_enabled_for (mode);
|
||||
}
|
||||
|
||||
void theme_policy_init (void)
|
||||
{
|
||||
}
|
||||
|
||||
void theme_policy_set_appearance_changed_callback (ThemePolicyAppearanceChangedFunc callback)
|
||||
{
|
||||
(void) callback;
|
||||
}
|
||||
|
||||
gboolean theme_application_apply_mode (unsigned int mode, gboolean *palette_changed)
|
||||
@@ -109,11 +122,12 @@ void theme_runtime_reset_mode_colors (gboolean dark_mode)
|
||||
(void) dark_mode;
|
||||
}
|
||||
|
||||
gboolean theme_runtime_apply_mode (unsigned int mode, gboolean *dark_active)
|
||||
gboolean theme_runtime_apply_mode (unsigned int mode, gboolean *palette_changed)
|
||||
{
|
||||
(void) mode;
|
||||
(void) dark_active;
|
||||
stub_apply_mode_calls++;
|
||||
if (palette_changed)
|
||||
*palette_changed = TRUE;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -249,10 +263,11 @@ test_manager_set_token_color_routes_by_mode (void)
|
||||
GdkRGBA color = { 0.1, 0.2, 0.3, 1.0 };
|
||||
gboolean palette_changed = FALSE;
|
||||
|
||||
/* All edits target the single user palette; the mode argument is
|
||||
* accepted for compatibility but no longer selects a dark palette. */
|
||||
reset_manager_stubs ();
|
||||
stub_policy_dark = FALSE;
|
||||
theme_manager_set_token_color (ZOITECHAT_DARK_MODE_LIGHT, THEME_TOKEN_MIRC_2, &color, &palette_changed);
|
||||
g_assert_cmpint (stub_policy_mode, ==, ZOITECHAT_DARK_MODE_LIGHT);
|
||||
g_assert_cmpint (stub_user_set_calls, ==, 1);
|
||||
g_assert_cmpint (stub_dark_set_calls, ==, 0);
|
||||
g_assert_cmpint (stub_apply_mode_calls, ==, 1);
|
||||
@@ -262,16 +277,14 @@ test_manager_set_token_color_routes_by_mode (void)
|
||||
reset_manager_stubs ();
|
||||
stub_policy_dark = TRUE;
|
||||
theme_manager_set_token_color (ZOITECHAT_DARK_MODE_DARK, THEME_TOKEN_MIRC_2, &color, &palette_changed);
|
||||
g_assert_cmpint (stub_policy_mode, ==, ZOITECHAT_DARK_MODE_DARK);
|
||||
g_assert_cmpint (stub_user_set_calls, ==, 0);
|
||||
g_assert_cmpint (stub_dark_set_calls, ==, 1);
|
||||
g_assert_cmpint (stub_user_set_calls, ==, 1);
|
||||
g_assert_cmpint (stub_dark_set_calls, ==, 0);
|
||||
|
||||
reset_manager_stubs ();
|
||||
stub_policy_dark = TRUE;
|
||||
theme_manager_set_token_color (ZOITECHAT_DARK_MODE_AUTO, THEME_TOKEN_MIRC_2, &color, &palette_changed);
|
||||
g_assert_cmpint (stub_policy_mode, ==, ZOITECHAT_DARK_MODE_AUTO);
|
||||
g_assert_cmpint (stub_user_set_calls, ==, 0);
|
||||
g_assert_cmpint (stub_dark_set_calls, ==, 1);
|
||||
g_assert_cmpint (stub_user_set_calls, ==, 1);
|
||||
g_assert_cmpint (stub_dark_set_calls, ==, 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -300,9 +313,9 @@ test_manager_set_token_color_routes_setup_indexes (void)
|
||||
stub_policy_dark = TRUE;
|
||||
palette_changed = FALSE;
|
||||
theme_manager_set_token_color (ZOITECHAT_DARK_MODE_DARK, def->token, &color, &palette_changed);
|
||||
g_assert_cmpint (stub_dark_set_calls, ==, 1);
|
||||
g_assert_cmpint (stub_last_dark_token, ==, def->token);
|
||||
g_assert_cmpint (stub_user_set_calls, ==, 0);
|
||||
g_assert_cmpint (stub_user_set_calls, ==, 1);
|
||||
g_assert_cmpint (stub_last_user_token, ==, def->token);
|
||||
g_assert_cmpint (stub_dark_set_calls, ==, 0);
|
||||
g_assert_true (palette_changed);
|
||||
}
|
||||
}
|
||||
@@ -345,6 +358,12 @@ test_manager_window_attach_detach_idempotence (void)
|
||||
gulong *second_handler_ptr;
|
||||
gulong second_handler_id;
|
||||
|
||||
if (!gtk_available)
|
||||
{
|
||||
g_test_message ("GTK display not available");
|
||||
return;
|
||||
}
|
||||
|
||||
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
|
||||
g_assert_nonnull (window);
|
||||
|
||||
@@ -395,5 +414,6 @@ main (int argc, char **argv)
|
||||
test_manager_listener_registration_dispatch_and_unregister);
|
||||
g_test_add_func ("/theme/manager/window_attach_detach_idempotence",
|
||||
test_manager_window_attach_detach_idempotence);
|
||||
gtk_available = gtk_init_check (&argc, &argv);
|
||||
return g_test_run ();
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ InputStyle *input_style;
|
||||
|
||||
static gboolean gtk_available;
|
||||
static int apply_current_calls;
|
||||
static int set_token_color_calls;
|
||||
static char applied_theme_id[256];
|
||||
static ThemeGtk3Variant applied_variant;
|
||||
static gboolean removed_selected;
|
||||
@@ -70,6 +71,15 @@ load_text_events (void)
|
||||
{
|
||||
}
|
||||
|
||||
char **
|
||||
text_color_event_names (int color, int *count)
|
||||
{
|
||||
(void)color;
|
||||
if (count)
|
||||
*count = 0;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
gboolean
|
||||
theme_get_color (ThemeSemanticToken token, GdkRGBA *color)
|
||||
{
|
||||
@@ -95,6 +105,7 @@ theme_manager_set_token_color (unsigned int dark_mode, ThemeSemanticToken token,
|
||||
(void)dark_mode;
|
||||
(void)token;
|
||||
(void)color;
|
||||
set_token_color_calls++;
|
||||
if (changed)
|
||||
*changed = FALSE;
|
||||
}
|
||||
@@ -107,12 +118,23 @@ theme_manager_reset_mode_colors (unsigned int mode, gboolean *palette_changed)
|
||||
*palette_changed = FALSE;
|
||||
}
|
||||
|
||||
void
|
||||
theme_runtime_clear_gtk_mapped_custom_tokens (void)
|
||||
{
|
||||
}
|
||||
|
||||
gboolean
|
||||
theme_manager_save_preferences (void)
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void
|
||||
theme_manager_apply_to_window (GtkWidget *window)
|
||||
{
|
||||
(void)window;
|
||||
}
|
||||
|
||||
void
|
||||
theme_manager_dispatch_changed (ThemeChangedReason reasons)
|
||||
{
|
||||
@@ -289,7 +311,7 @@ theme_gtk3_is_active (void)
|
||||
#include "../theme-preferences.c"
|
||||
|
||||
static void
|
||||
test_removed_selected_theme_commits_fallback_and_applies (void)
|
||||
test_removed_selected_theme_falls_back_to_system_default (void)
|
||||
{
|
||||
GtkWidget *page;
|
||||
theme_preferences_ui *ui;
|
||||
@@ -306,23 +328,26 @@ test_removed_selected_theme_commits_fallback_and_applies (void)
|
||||
g_strlcpy (prefs.hex_gui_gtk3_theme, "removed-theme", sizeof (prefs.hex_gui_gtk3_theme));
|
||||
prefs.hex_gui_gtk3_variant = THEME_GTK3_VARIANT_PREFER_DARK;
|
||||
removed_selected = FALSE;
|
||||
apply_current_calls = 0;
|
||||
applied_theme_id[0] = '\0';
|
||||
|
||||
page = theme_preferences_create_page (NULL, &setup_prefs, NULL);
|
||||
ui = g_object_get_data (G_OBJECT (page), "theme-preferences-ui");
|
||||
g_assert_nonnull (ui);
|
||||
|
||||
apply_current_calls = 0;
|
||||
applied_theme_id[0] = '\0';
|
||||
|
||||
g_assert_nonnull (ui->gtk3_remove);
|
||||
gtk_button_clicked (GTK_BUTTON (ui->gtk3_remove));
|
||||
|
||||
g_assert_cmpstr (prefs.hex_gui_gtk3_theme, ==, "fallback-theme");
|
||||
g_assert_cmpstr (setup_prefs.hex_gui_gtk3_theme, ==, "fallback-theme");
|
||||
g_assert_cmpint (prefs.hex_gui_gtk3_variant, ==, THEME_GTK3_VARIANT_PREFER_LIGHT);
|
||||
g_assert_cmpint (setup_prefs.hex_gui_gtk3_variant, ==, THEME_GTK3_VARIANT_PREFER_LIGHT);
|
||||
/* Removing the active in-app theme returns to the system GTK3
|
||||
* theme ("None") instead of forcing another theme. */
|
||||
g_assert_cmpstr (prefs.hex_gui_gtk3_theme, ==, "");
|
||||
g_assert_cmpstr (setup_prefs.hex_gui_gtk3_theme, ==, "");
|
||||
g_assert_cmpint (prefs.hex_gui_gtk3_variant, ==, THEME_GTK3_VARIANT_FOLLOW_SYSTEM);
|
||||
g_assert_cmpint (setup_prefs.hex_gui_gtk3_variant, ==, THEME_GTK3_VARIANT_FOLLOW_SYSTEM);
|
||||
g_assert_cmpint (apply_current_calls, ==, 1);
|
||||
g_assert_cmpstr (applied_theme_id, ==, "fallback-theme");
|
||||
g_assert_cmpint (applied_variant, ==, THEME_GTK3_VARIANT_PREFER_LIGHT);
|
||||
g_assert_cmpstr (applied_theme_id, ==, "");
|
||||
g_assert_cmpint (applied_variant, ==, THEME_GTK3_VARIANT_FOLLOW_SYSTEM);
|
||||
|
||||
gtk_widget_destroy (page);
|
||||
}
|
||||
@@ -396,16 +421,55 @@ test_select_none_resets_theme_and_applies (void)
|
||||
gtk_widget_destroy (page);
|
||||
}
|
||||
|
||||
static void
|
||||
test_select_theme_does_not_pin_palette_tokens (void)
|
||||
{
|
||||
GtkWidget *page;
|
||||
theme_preferences_ui *ui;
|
||||
struct zoitechatprefs setup_prefs;
|
||||
|
||||
if (!gtk_available)
|
||||
{
|
||||
g_test_message ("GTK display not available");
|
||||
return;
|
||||
}
|
||||
|
||||
memset (&setup_prefs, 0, sizeof (setup_prefs));
|
||||
memset (&prefs, 0, sizeof (prefs));
|
||||
removed_selected = FALSE;
|
||||
apply_current_calls = 0;
|
||||
set_token_color_calls = 0;
|
||||
|
||||
page = theme_preferences_create_page (NULL, &setup_prefs, NULL);
|
||||
ui = g_object_get_data (G_OBJECT (page), "theme-preferences-ui");
|
||||
g_assert_nonnull (ui);
|
||||
|
||||
/* 0 = None, 1 = removed-theme, 2 = fallback-theme */
|
||||
gtk_combo_box_set_active (GTK_COMBO_BOX (ui->gtk3_combo), 2);
|
||||
|
||||
g_assert_cmpstr (prefs.hex_gui_gtk3_theme, ==, "fallback-theme");
|
||||
g_assert_cmpint (apply_current_calls, ==, 1);
|
||||
/* Selecting a GTK3 theme must leave the mapped palette tokens
|
||||
* unpinned so the user list, channel tree and input box keep
|
||||
* resolving their colors from the active theme instead of being
|
||||
* frozen until the next restart. */
|
||||
g_assert_cmpint (set_token_color_calls, ==, 0);
|
||||
|
||||
gtk_widget_destroy (page);
|
||||
}
|
||||
|
||||
int
|
||||
main (int argc, char **argv)
|
||||
{
|
||||
g_test_init (&argc, &argv, NULL);
|
||||
gtk_available = gtk_init_check (&argc, &argv);
|
||||
g_test_add_func ("/theme/preferences/gtk3_removed_selection_applies_fallback",
|
||||
test_removed_selected_theme_commits_fallback_and_applies);
|
||||
g_test_add_func ("/theme/preferences/gtk3_removed_selection_falls_back_to_system",
|
||||
test_removed_selected_theme_falls_back_to_system_default);
|
||||
g_test_add_func ("/theme/preferences/gtk3_unset_keeps_system_default",
|
||||
test_unset_theme_keeps_system_default_without_apply);
|
||||
g_test_add_func ("/theme/preferences/gtk3_select_none_resets_theme",
|
||||
test_select_none_resets_theme_and_applies);
|
||||
g_test_add_func ("/theme/preferences/gtk3_select_theme_does_not_pin_palette_tokens",
|
||||
test_select_theme_does_not_pin_palette_tokens);
|
||||
return g_test_run ();
|
||||
}
|
||||
|
||||
@@ -290,12 +290,12 @@ test_ui_edits_persist_without_legacy_array_mutation (void)
|
||||
theme_runtime_save ();
|
||||
|
||||
theme_runtime_load ();
|
||||
theme_runtime_apply_mode (ZOITECHAT_DARK_MODE_LIGHT, NULL);
|
||||
theme_runtime_apply_dark_mode (FALSE);
|
||||
g_assert_true (theme_runtime_get_color (THEME_TOKEN_SELECTION_FOREGROUND, &light_loaded));
|
||||
g_assert_true (gdk_rgba_parse (&light_expected, "#224466"));
|
||||
g_assert_true (colors_equal (&light_loaded, &light_expected));
|
||||
|
||||
theme_runtime_apply_mode (ZOITECHAT_DARK_MODE_DARK, NULL);
|
||||
theme_runtime_apply_dark_mode (TRUE);
|
||||
g_assert_true (theme_runtime_get_color (THEME_TOKEN_SELECTION_FOREGROUND, &dark_loaded));
|
||||
g_assert_true (gdk_rgba_parse (&dark_expected, "#88aacc"));
|
||||
g_assert_true (colors_equal (&dark_loaded, &dark_expected));
|
||||
|
||||
@@ -20,8 +20,9 @@
|
||||
|
||||
#include "theme-access.h"
|
||||
|
||||
#include "theme-runtime.h"
|
||||
#include "theme-css.h"
|
||||
#include "theme-gtk3.h"
|
||||
#include "theme-runtime.h"
|
||||
|
||||
|
||||
|
||||
@@ -32,7 +33,8 @@ enum
|
||||
THEME_XTEXT_MARK_BG_INDEX = 100,
|
||||
THEME_XTEXT_FG_INDEX = 101,
|
||||
THEME_XTEXT_BG_INDEX = 102,
|
||||
THEME_XTEXT_MARKER_INDEX = 103
|
||||
THEME_XTEXT_MARKER_INDEX = 103,
|
||||
THEME_XTEXT_MARKER_LEGACY_INDEX = 36
|
||||
};
|
||||
|
||||
static const guint8 theme_default_99_mirc_colors[THEME_XTEXT_MIRC_COLS][3] = {
|
||||
@@ -52,14 +54,13 @@ static const guint8 theme_default_99_mirc_colors[THEME_XTEXT_MIRC_COLS][3] = {
|
||||
};
|
||||
|
||||
static void
|
||||
theme_access_apply_default_99_palette (XTextColor *palette, size_t palette_len, gboolean apply_base)
|
||||
theme_access_apply_default_99_palette (XTextColor *palette, size_t palette_len)
|
||||
{
|
||||
size_t i;
|
||||
size_t start = apply_base ? 0 : 32;
|
||||
|
||||
if (palette_len == 0)
|
||||
return;
|
||||
for (i = start; i < THEME_XTEXT_MIRC_COLS && i < palette_len; i++)
|
||||
for (i = 32; i < THEME_XTEXT_MIRC_COLS && i < palette_len; i++)
|
||||
{
|
||||
palette[i].red = theme_default_99_mirc_colors[i][0] / 255.0;
|
||||
palette[i].green = theme_default_99_mirc_colors[i][1] / 255.0;
|
||||
@@ -112,28 +113,95 @@ theme_access_context_get_background_color (GtkStyleContext *context, GtkStateFla
|
||||
gtk_style_context_restore (context);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
theme_access_lookup_named_color (GtkStyleContext *context, const char *const *names, GdkRGBA *out_color)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
/* With an in-app GTK3 theme selected, prefer the colors resolved from
|
||||
* the theme itself: desktop integration CSS (e.g. KDE's color-scheme
|
||||
* sync) redefines the same named colors with the OS palette in the
|
||||
* widget cascade, which would drag OS dark/light colors into the
|
||||
* mapped palette. */
|
||||
for (i = 0; names[i] != NULL; i++)
|
||||
{
|
||||
if (theme_gtk3_lookup_theme_color (names[i], out_color))
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
for (i = 0; names[i] != NULL; i++)
|
||||
{
|
||||
if (gtk_style_context_lookup_color (context, names[i], out_color))
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static void
|
||||
theme_access_resolve_map_background (GtkStyleContext *context, GtkStateFlags state,
|
||||
const char *const *named_fallbacks, GdkRGBA *out_color)
|
||||
{
|
||||
GdkRGBA named;
|
||||
|
||||
theme_access_context_get_background_color (context, state, out_color);
|
||||
if (out_color->alpha > 0.0)
|
||||
return;
|
||||
|
||||
/* Unstyled CSS nodes (e.g. the custom chat text widget) report a fully
|
||||
* transparent background; fall back to the theme's named colors so the
|
||||
* mapped palette still follows the active GTK3 theme. */
|
||||
if (theme_access_lookup_named_color (context, named_fallbacks, &named))
|
||||
*out_color = named;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
theme_access_get_gtk_palette_map (GtkWidget *widget, ThemeGtkPaletteMap *out_map)
|
||||
{
|
||||
static const char *const base_fallbacks[] = { "theme_base_color", "theme_bg_color", NULL };
|
||||
static const char *const selected_bg_fallbacks[] = { "theme_selected_bg_color", NULL };
|
||||
static const char *const selected_fg_fallbacks[] = { "theme_selected_fg_color", NULL };
|
||||
GtkStyleContext *context;
|
||||
const char *palette_class;
|
||||
gboolean had_palette_class;
|
||||
GdkRGBA named;
|
||||
GdkRGBA accent;
|
||||
|
||||
g_return_val_if_fail (out_map != NULL, FALSE);
|
||||
if (!theme_gtk3_is_active () || widget == NULL || !GTK_IS_WIDGET (widget))
|
||||
if (widget == NULL || !GTK_IS_WIDGET (widget))
|
||||
return FALSE;
|
||||
|
||||
context = gtk_widget_get_style_context (widget);
|
||||
if (context == NULL)
|
||||
return FALSE;
|
||||
|
||||
/* The application paints these widgets with fixed colors through the
|
||||
* zoitechat-palette provider. Sampling must see the underlying GTK3
|
||||
* theme, not the colors we applied earlier: otherwise every theme
|
||||
* change reads back the previous palette and the user list, channel
|
||||
* tree and input box keep their stale colors until a restart. */
|
||||
palette_class = theme_css_palette_class_name ();
|
||||
had_palette_class = gtk_style_context_has_class (context, palette_class);
|
||||
if (had_palette_class)
|
||||
gtk_style_context_remove_class (context, palette_class);
|
||||
|
||||
theme_access_context_get_color (context, GTK_STATE_FLAG_NORMAL, &out_map->text_foreground);
|
||||
theme_access_context_get_background_color (context, GTK_STATE_FLAG_NORMAL, &out_map->text_background);
|
||||
theme_access_resolve_map_background (context, GTK_STATE_FLAG_NORMAL, base_fallbacks,
|
||||
&out_map->text_background);
|
||||
theme_access_context_get_color (context, GTK_STATE_FLAG_SELECTED, &out_map->selection_foreground);
|
||||
theme_access_context_get_background_color (context, GTK_STATE_FLAG_SELECTED, &out_map->selection_background);
|
||||
theme_access_resolve_map_background (context, GTK_STATE_FLAG_SELECTED, selected_bg_fallbacks,
|
||||
&out_map->selection_background);
|
||||
if (gdk_rgba_equal (&out_map->selection_foreground, &out_map->text_foreground) &&
|
||||
theme_access_lookup_named_color (context, selected_fg_fallbacks, &named))
|
||||
out_map->selection_foreground = named;
|
||||
theme_access_context_get_color (context, GTK_STATE_FLAG_LINK, &accent);
|
||||
if (accent.alpha <= 0.0)
|
||||
accent = out_map->selection_background;
|
||||
out_map->accent = accent;
|
||||
|
||||
if (had_palette_class)
|
||||
gtk_style_context_add_class (context, palette_class);
|
||||
|
||||
out_map->enabled = TRUE;
|
||||
return TRUE;
|
||||
}
|
||||
@@ -148,11 +216,10 @@ gboolean
|
||||
theme_get_mirc_color (unsigned int mirc_index, GdkRGBA *out_rgba)
|
||||
{
|
||||
ThemeSemanticToken token = (ThemeSemanticToken) (THEME_TOKEN_MIRC_0 + (int) mirc_index);
|
||||
gboolean has_user_colors = theme_runtime_mode_has_user_colors (theme_runtime_is_dark_active ());
|
||||
|
||||
if (mirc_index >= THEME_XTEXT_MIRC_COLS)
|
||||
return FALSE;
|
||||
if (!has_user_colors || mirc_index >= 32)
|
||||
if (mirc_index >= 32)
|
||||
{
|
||||
out_rgba->red = theme_default_99_mirc_colors[mirc_index][0] / 255.0;
|
||||
out_rgba->green = theme_default_99_mirc_colors[mirc_index][1] / 255.0;
|
||||
@@ -173,11 +240,10 @@ gboolean
|
||||
theme_get_mirc_color_rgb16 (unsigned int mirc_index, guint16 *red, guint16 *green, guint16 *blue)
|
||||
{
|
||||
ThemeSemanticToken token = (ThemeSemanticToken) (THEME_TOKEN_MIRC_0 + (int) mirc_index);
|
||||
gboolean has_user_colors = theme_runtime_mode_has_user_colors (theme_runtime_is_dark_active ());
|
||||
|
||||
if (mirc_index >= THEME_XTEXT_MIRC_COLS)
|
||||
return FALSE;
|
||||
if (!has_user_colors || mirc_index >= 32)
|
||||
if (mirc_index >= 32)
|
||||
{
|
||||
*red = (guint16) (theme_default_99_mirc_colors[mirc_index][0] * 257);
|
||||
*green = (guint16) (theme_default_99_mirc_colors[mirc_index][1] * 257);
|
||||
@@ -226,18 +292,37 @@ theme_get_xtext_colors (XTextColor *palette, size_t palette_len)
|
||||
void
|
||||
theme_get_xtext_colors_for_widget (GtkWidget *widget, XTextColor *palette, size_t palette_len)
|
||||
{
|
||||
ThemeGtkPaletteMap gtk_map = { 0 };
|
||||
ThemeWidgetStyleValues style_values;
|
||||
gboolean has_user_colors;
|
||||
GdkRGBA marker_color;
|
||||
gboolean have_marker = FALSE;
|
||||
|
||||
if (!palette)
|
||||
return;
|
||||
|
||||
theme_get_widget_style_values_for_widget (widget, &style_values);
|
||||
theme_runtime_get_xtext_colors (palette, palette_len);
|
||||
has_user_colors = theme_runtime_mode_has_user_colors (theme_runtime_is_dark_active ());
|
||||
if (theme_access_get_gtk_palette_map (widget, >k_map))
|
||||
{
|
||||
theme_runtime_get_widget_style_values_mapped (>k_map, &style_values);
|
||||
theme_runtime_get_xtext_colors_mapped (>k_map, palette, palette_len);
|
||||
}
|
||||
else
|
||||
{
|
||||
theme_runtime_get_widget_style_values (&style_values);
|
||||
theme_runtime_get_xtext_colors (palette, palette_len);
|
||||
}
|
||||
if (palette_len > THEME_XTEXT_MARKER_LEGACY_INDEX)
|
||||
{
|
||||
/* The marker token lives at its legacy slot before the extended
|
||||
* mIRC palette overwrites it; keep the (possibly theme-mapped)
|
||||
* value for the dedicated marker index below. */
|
||||
marker_color.red = palette[THEME_XTEXT_MARKER_LEGACY_INDEX].red;
|
||||
marker_color.green = palette[THEME_XTEXT_MARKER_LEGACY_INDEX].green;
|
||||
marker_color.blue = palette[THEME_XTEXT_MARKER_LEGACY_INDEX].blue;
|
||||
marker_color.alpha = palette[THEME_XTEXT_MARKER_LEGACY_INDEX].alpha;
|
||||
have_marker = TRUE;
|
||||
}
|
||||
if (palette_len >= THEME_XTEXT_MIRC_COLS)
|
||||
theme_access_apply_default_99_palette (palette, palette_len, !has_user_colors);
|
||||
theme_access_apply_default_99_palette (palette, palette_len);
|
||||
if (palette_len > THEME_XTEXT_MARK_FG_INDEX)
|
||||
{
|
||||
palette[THEME_XTEXT_MARK_FG_INDEX].red = style_values.selection_foreground.red;
|
||||
@@ -254,7 +339,7 @@ theme_get_xtext_colors_for_widget (GtkWidget *widget, XTextColor *palette, size_
|
||||
}
|
||||
if (palette_len > THEME_XTEXT_MARKER_INDEX)
|
||||
{
|
||||
if (!theme_runtime_get_color (THEME_TOKEN_MARKER, &marker_color))
|
||||
if (!have_marker && !theme_runtime_get_color (THEME_TOKEN_MARKER, &marker_color))
|
||||
marker_color = style_values.selection_background;
|
||||
palette[THEME_XTEXT_MARKER_INDEX].red = marker_color.red;
|
||||
palette[THEME_XTEXT_MARKER_INDEX].green = marker_color.green;
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "theme-css.h"
|
||||
#include "theme-runtime.h"
|
||||
#include "theme-gtk3.h"
|
||||
#include "theme-policy.h"
|
||||
#include "../maingui.h"
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
@@ -55,10 +56,19 @@ theme_application_apply_toplevel_theme (gboolean dark)
|
||||
|
||||
if (theme_gtk3_is_active ())
|
||||
{
|
||||
if (prefs.hex_gui_gtk3_variant == THEME_GTK3_VARIANT_PREFER_DARK)
|
||||
prefer_dark = TRUE;
|
||||
else if (prefs.hex_gui_gtk3_variant == THEME_GTK3_VARIANT_PREFER_LIGHT)
|
||||
prefer_dark = FALSE;
|
||||
/* An explicitly selected in-app theme controls its own
|
||||
* variant; the OS light/dark preference must not leak into
|
||||
* the window chrome (menu bar, dialogs). The active variant
|
||||
* is always resolved to light or dark. */
|
||||
prefer_dark = theme_gtk3_active_variant () == THEME_GTK3_VARIANT_PREFER_DARK;
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Following the system GTK3 theme: only request the dark
|
||||
* variant when the dark-mode preference (or the system, in
|
||||
* auto mode) asks for it, instead of clobbering the value
|
||||
* that came from the user's GTK configuration. */
|
||||
prefer_dark = theme_policy_is_dark_mode_active (prefs.hex_gui_dark_mode);
|
||||
}
|
||||
|
||||
if (settings && g_object_class_find_property (G_OBJECT_GET_CLASS (settings),
|
||||
@@ -88,9 +98,15 @@ theme_application_apply_toplevel_theme (gboolean dark)
|
||||
gboolean
|
||||
theme_application_apply_mode (unsigned int mode, gboolean *palette_changed)
|
||||
{
|
||||
static gboolean runtime_loaded = FALSE;
|
||||
gboolean dark;
|
||||
|
||||
theme_runtime_load ();
|
||||
if (!runtime_loaded)
|
||||
{
|
||||
theme_runtime_load ();
|
||||
runtime_loaded = TRUE;
|
||||
}
|
||||
|
||||
dark = theme_runtime_apply_mode (mode, palette_changed);
|
||||
theme_application_apply_toplevel_theme (dark);
|
||||
|
||||
|
||||
@@ -29,8 +29,6 @@
|
||||
static const char *theme_css_selector_input = "#zoitechat-inputbox";
|
||||
static const char *theme_css_selector_input_text = "#zoitechat-inputbox text";
|
||||
static const char *theme_css_selector_palette_class = "zoitechat-palette";
|
||||
static const char *theme_css_selector_dark_class = "zoitechat-dark";
|
||||
static const char *theme_css_selector_light_class = "zoitechat-light";
|
||||
static const char *theme_css_palette_provider_key = "zoitechat-palette-provider";
|
||||
static const guint theme_css_provider_priority = GTK_STYLE_PROVIDER_PRIORITY_USER;
|
||||
static const char *theme_css_palette_scope_selectors =
|
||||
@@ -62,6 +60,12 @@ typedef struct
|
||||
static GtkCssProvider *theme_css_input_provider;
|
||||
static ThemeCssInputFingerprint theme_css_input_fp;
|
||||
|
||||
const char *
|
||||
theme_css_palette_class_name (void)
|
||||
{
|
||||
return theme_css_selector_palette_class;
|
||||
}
|
||||
|
||||
void
|
||||
theme_css_apply_app_provider (GtkStyleProvider *provider)
|
||||
{
|
||||
@@ -362,61 +366,9 @@ theme_css_apply_palette_widget (GtkWidget *widget, const GdkRGBA *bg, const GdkR
|
||||
char *
|
||||
theme_css_build_toplevel_classes (void)
|
||||
{
|
||||
return g_strdup_printf (
|
||||
"window.%s, window.%s:backdrop, .%s {"
|
||||
"background-color: #202020;"
|
||||
"color: #f0f0f0;"
|
||||
"border-color: #202020;"
|
||||
"}"
|
||||
"window.%s menubar, window.%s menubar:backdrop, window.%s menubar box, window.%s menubar box:backdrop, window.%s menuitem, window.%s menuitem:backdrop {"
|
||||
"background-color: @theme_bg_color;"
|
||||
"background-image: none;"
|
||||
"color: @theme_fg_color;"
|
||||
"border-color: @theme_bg_color;"
|
||||
"}"
|
||||
"window.%s menuitem:hover, window.%s menuitem:selected {"
|
||||
"background-color: @theme_selected_bg_color;"
|
||||
"background-image: none;"
|
||||
"color: @theme_selected_fg_color;"
|
||||
"border-color: @theme_selected_bg_color;"
|
||||
"}"
|
||||
"window.%s, window.%s:backdrop, .%s {"
|
||||
"background-color: #f6f6f6;"
|
||||
"color: #101010;"
|
||||
"border-color: #f6f6f6;"
|
||||
"}"
|
||||
"window.%s menubar, window.%s menubar:backdrop, window.%s menubar box, window.%s menubar box:backdrop, window.%s menuitem, window.%s menuitem:backdrop {"
|
||||
"background-color: @theme_bg_color;"
|
||||
"background-image: none;"
|
||||
"color: @theme_fg_color;"
|
||||
"border-color: @theme_bg_color;"
|
||||
"}"
|
||||
"window.%s menuitem:hover, window.%s menuitem:selected {"
|
||||
"background-color: @theme_selected_bg_color;"
|
||||
"background-image: none;"
|
||||
"color: @theme_selected_fg_color;"
|
||||
"border-color: @theme_selected_bg_color;"
|
||||
"}",
|
||||
theme_css_selector_dark_class,
|
||||
theme_css_selector_dark_class,
|
||||
theme_css_selector_dark_class,
|
||||
theme_css_selector_dark_class,
|
||||
theme_css_selector_dark_class,
|
||||
theme_css_selector_dark_class,
|
||||
theme_css_selector_dark_class,
|
||||
theme_css_selector_dark_class,
|
||||
theme_css_selector_dark_class,
|
||||
theme_css_selector_dark_class,
|
||||
theme_css_selector_dark_class,
|
||||
theme_css_selector_light_class,
|
||||
theme_css_selector_light_class,
|
||||
theme_css_selector_light_class,
|
||||
theme_css_selector_light_class,
|
||||
theme_css_selector_light_class,
|
||||
theme_css_selector_light_class,
|
||||
theme_css_selector_light_class,
|
||||
theme_css_selector_light_class,
|
||||
theme_css_selector_light_class,
|
||||
theme_css_selector_light_class,
|
||||
theme_css_selector_light_class);
|
||||
/* Windows, dialogs and menus must render with the colors of the active
|
||||
* GTK3 theme (system or in-app selected). zoitechat-dark and
|
||||
* zoitechat-light are marker classes only; painting fixed colors here
|
||||
* overrides every theme (issue #334). */
|
||||
return g_strdup ("");
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
* theme_css_apply_widget_provider:
|
||||
* Use for widget-local CSS providers attached to a specific widget context.
|
||||
*/
|
||||
const char *theme_css_palette_class_name (void);
|
||||
void theme_css_apply_app_provider (GtkStyleProvider *provider);
|
||||
void theme_css_remove_app_provider (GtkStyleProvider *provider);
|
||||
void theme_css_apply_widget_provider (GtkWidget *widget, GtkStyleProvider *provider);
|
||||
|
||||
@@ -33,11 +33,60 @@
|
||||
|
||||
static GPtrArray *theme_gtk3_providers_base;
|
||||
static GPtrArray *theme_gtk3_providers_variant;
|
||||
static GtkCssProvider *theme_gtk3_chrome_provider;
|
||||
static GHashTable *theme_gtk3_theme_colors;
|
||||
static GHashTable *theme_gtk3_provider_cache;
|
||||
static gboolean theme_gtk3_active;
|
||||
static char *theme_gtk3_current_id;
|
||||
static ThemeGtk3Variant theme_gtk3_current_variant;
|
||||
|
||||
/* Menu bar and menus fall back to the base gtk-theme-name theme (the OS
|
||||
* theme) for any property the selected in-app theme's CSS does not define,
|
||||
* which mixes OS dark/light colors into the chrome (issue: dark menu bar on
|
||||
* a light in-app theme when the OS is in dark mode). While an in-app theme
|
||||
* is active, backfill those widgets with the theme's own named colors. The
|
||||
* provider sits just below the theme providers, so a theme that styles its
|
||||
* menus keeps full control. */
|
||||
#define THEME_GTK3_CHROME_PRIORITY (GTK_STYLE_PROVIDER_PRIORITY_USER - 1)
|
||||
/* Concrete color values are formatted in: desktop integration CSS (e.g.
|
||||
* KDE's color-scheme sync in ~/.config/gtk-3.0/gtk.css) redefines the
|
||||
* standard named colors with the OS palette at USER priority, so a
|
||||
* "@theme_bg_color" reference here would resolve to the OS dark/light
|
||||
* colors instead of the selected theme's. */
|
||||
static const char theme_gtk3_chrome_css_template[] =
|
||||
".background, .background:backdrop {"
|
||||
" background-color: %s;"
|
||||
" color: %s;"
|
||||
"}"
|
||||
"headerbar, headerbar:backdrop, .titlebar, .titlebar:backdrop {"
|
||||
" background-color: %s;"
|
||||
" background-image: none;"
|
||||
" color: %s;"
|
||||
"}";
|
||||
|
||||
/* The menu bar body and the menu items are separate CSS surfaces; desktop
|
||||
* integration CSS or a theme can style one but not the other, splitting
|
||||
* the menu bar row into two colors. These rules sit above the theme and
|
||||
* the desktop CSS so body and items always render as one surface. */
|
||||
#define THEME_GTK3_MENU_UNIFORM_PRIORITY (GTK_STYLE_PROVIDER_PRIORITY_USER + 40)
|
||||
static const char theme_gtk3_menu_uniform_template[] =
|
||||
"menubar, menubar:backdrop, menubar > menuitem, menubar > menuitem:backdrop,"
|
||||
"menu, menu:backdrop, .menu, .menu:backdrop, menu > menuitem, menu > menuitem:backdrop {"
|
||||
" background-color: %s;"
|
||||
" background-image: none;"
|
||||
" color: %s;"
|
||||
"}"
|
||||
"menubar > menuitem:hover, menubar > menuitem:selected,"
|
||||
"menu > menuitem:hover, menu > menuitem:selected,"
|
||||
".menu menuitem:hover, .menu menuitem:selected {"
|
||||
" background-color: %s;"
|
||||
" background-image: none;"
|
||||
" color: %s;"
|
||||
"}"
|
||||
"menubar > menuitem:disabled, menu > menuitem:disabled {"
|
||||
" color: alpha(%s, 0.5);"
|
||||
"}";
|
||||
|
||||
typedef struct
|
||||
{
|
||||
GHashTable *defaults;
|
||||
@@ -49,6 +98,7 @@ typedef struct
|
||||
static ThemeGtk3SettingsState theme_gtk3_settings_state;
|
||||
|
||||
static gboolean settings_apply_property (GtkSettings *settings, const char *property_name, const char *raw_value);
|
||||
static void theme_gtk3_menu_uniform_apply (const char *bg, const char *fg, const char *sel_bg, const char *sel_fg);
|
||||
|
||||
static gboolean
|
||||
theme_gtk3_theme_name_is_dark (const char *name)
|
||||
@@ -135,8 +185,12 @@ settings_defaults_table (void)
|
||||
static void
|
||||
settings_rescan_icon_theme (void)
|
||||
{
|
||||
GdkScreen *screen = gdk_screen_get_default ();
|
||||
GtkIconTheme *icon_theme;
|
||||
|
||||
if (!screen)
|
||||
return;
|
||||
|
||||
icon_theme = gtk_icon_theme_get_default ();
|
||||
if (!icon_theme)
|
||||
return;
|
||||
@@ -156,8 +210,13 @@ theme_gtk3_reset_widgets (void)
|
||||
static void
|
||||
settings_capture_icon_search_path (void)
|
||||
{
|
||||
GtkIconTheme *icon_theme = gtk_icon_theme_get_default ();
|
||||
GdkScreen *screen = gdk_screen_get_default ();
|
||||
GtkIconTheme *icon_theme;
|
||||
|
||||
if (!screen)
|
||||
return;
|
||||
|
||||
icon_theme = gtk_icon_theme_get_default ();
|
||||
if (!icon_theme || theme_gtk3_settings_state.icon_search_path_captured)
|
||||
return;
|
||||
|
||||
@@ -168,8 +227,13 @@ settings_capture_icon_search_path (void)
|
||||
static void
|
||||
settings_append_icon_search_path (const char *path)
|
||||
{
|
||||
GtkIconTheme *icon_theme = gtk_icon_theme_get_default ();
|
||||
GdkScreen *screen = gdk_screen_get_default ();
|
||||
GtkIconTheme *icon_theme;
|
||||
|
||||
if (!screen)
|
||||
return;
|
||||
|
||||
icon_theme = gtk_icon_theme_get_default ();
|
||||
if (!icon_theme || !path || !g_file_test (path, G_FILE_TEST_IS_DIR))
|
||||
return;
|
||||
|
||||
@@ -199,8 +263,13 @@ settings_apply_icon_paths (const char *theme_root)
|
||||
static void
|
||||
settings_restore_icon_search_path (void)
|
||||
{
|
||||
GtkIconTheme *icon_theme = gtk_icon_theme_get_default ();
|
||||
GdkScreen *screen = gdk_screen_get_default ();
|
||||
GtkIconTheme *icon_theme;
|
||||
|
||||
if (!screen)
|
||||
return;
|
||||
|
||||
icon_theme = gtk_icon_theme_get_default ();
|
||||
if (!icon_theme || !theme_gtk3_settings_state.icon_search_path_captured)
|
||||
return;
|
||||
|
||||
@@ -380,6 +449,21 @@ settings_theme_link_search_path (const char *theme_root, const char *theme_name)
|
||||
}
|
||||
|
||||
link_path = g_build_filename (themes_root, theme_name, NULL);
|
||||
|
||||
/* A leftover symlink from a removed or relocated theme would satisfy
|
||||
* the EXISTS check while pointing at the wrong (or no) theme, leaving
|
||||
* the base gtk-theme-name on the OS theme. Replace it. */
|
||||
{
|
||||
char *link_target = g_file_read_link (link_path, NULL);
|
||||
if (link_target)
|
||||
{
|
||||
if (g_strcmp0 (link_target, theme_root) != 0 ||
|
||||
!g_file_test (link_path, G_FILE_TEST_IS_DIR))
|
||||
g_unlink (link_path);
|
||||
g_free (link_target);
|
||||
}
|
||||
}
|
||||
|
||||
if (!g_file_test (link_path, G_FILE_TEST_EXISTS))
|
||||
{
|
||||
GFile *link_file = g_file_new_for_path (link_path);
|
||||
@@ -394,6 +478,14 @@ settings_theme_link_search_path (const char *theme_root, const char *theme_name)
|
||||
return ok;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
settings_default_seat_available (void)
|
||||
{
|
||||
GdkDisplay *display = gdk_display_get_default ();
|
||||
|
||||
return display && GDK_IS_SEAT (gdk_display_get_default_seat (display));
|
||||
}
|
||||
|
||||
static void
|
||||
settings_apply_theme_name (const char *theme_root)
|
||||
{
|
||||
@@ -404,7 +496,7 @@ settings_apply_theme_name (const char *theme_root)
|
||||
return;
|
||||
|
||||
settings = gtk_settings_get_default ();
|
||||
if (!settings)
|
||||
if (!settings || !settings_default_seat_available ())
|
||||
return;
|
||||
|
||||
theme_name = g_path_get_basename (theme_root);
|
||||
@@ -730,12 +822,176 @@ settings_apply_from_file (const char *theme_root, const char *css_dir)
|
||||
g_ptr_array_unref (settings_paths);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
theme_gtk3_providers_define_color (const char *name)
|
||||
{
|
||||
char *needle = g_strdup_printf ("@define-color %s ", name);
|
||||
gboolean found = FALSE;
|
||||
guint i;
|
||||
|
||||
for (i = 0; !found && theme_gtk3_providers_base && i < theme_gtk3_providers_base->len; i++)
|
||||
{
|
||||
char *dump = gtk_css_provider_to_string (g_ptr_array_index (theme_gtk3_providers_base, i));
|
||||
found = dump && strstr (dump, needle) != NULL;
|
||||
g_free (dump);
|
||||
}
|
||||
for (i = 0; !found && theme_gtk3_providers_variant && i < theme_gtk3_providers_variant->len; i++)
|
||||
{
|
||||
char *dump = gtk_css_provider_to_string (g_ptr_array_index (theme_gtk3_providers_variant, i));
|
||||
found = dump && strstr (dump, needle) != NULL;
|
||||
g_free (dump);
|
||||
}
|
||||
|
||||
g_free (needle);
|
||||
return found;
|
||||
}
|
||||
|
||||
/* Resolve a named color from the selected theme itself. The lookup
|
||||
* context layers the theme's providers over the screen cascade, so the
|
||||
* theme's own @define-color wins; when the theme does not define the
|
||||
* name at all, fall back to a neutral palette matching the theme's
|
||||
* variant instead of whatever the desktop injected into the cascade. */
|
||||
static char *
|
||||
theme_gtk3_chrome_resolve_color (GtkStyleContext *lookup, const char *name, const char *fallback)
|
||||
{
|
||||
GdkRGBA color;
|
||||
|
||||
if (!theme_gtk3_providers_define_color (name) ||
|
||||
!gtk_style_context_lookup_color (lookup, name, &color))
|
||||
g_assert (gdk_rgba_parse (&color, fallback));
|
||||
|
||||
if (!theme_gtk3_theme_colors)
|
||||
theme_gtk3_theme_colors = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
|
||||
{
|
||||
GdkRGBA *stored = g_new (GdkRGBA, 1);
|
||||
*stored = color;
|
||||
g_hash_table_insert (theme_gtk3_theme_colors, g_strdup (name), stored);
|
||||
}
|
||||
|
||||
return gdk_rgba_to_string (&color);
|
||||
}
|
||||
|
||||
gboolean
|
||||
theme_gtk3_lookup_theme_color (const char *name, GdkRGBA *out_color)
|
||||
{
|
||||
GdkRGBA *stored;
|
||||
|
||||
if (!theme_gtk3_active || !theme_gtk3_theme_colors || !name || !out_color)
|
||||
return FALSE;
|
||||
|
||||
stored = g_hash_table_lookup (theme_gtk3_theme_colors, name);
|
||||
if (!stored)
|
||||
return FALSE;
|
||||
|
||||
*out_color = *stored;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
theme_gtk3_chrome_provider_apply (gboolean prefer_dark)
|
||||
{
|
||||
GdkScreen *screen = gdk_screen_get_default ();
|
||||
GtkStyleContext *lookup;
|
||||
char *bg;
|
||||
char *fg;
|
||||
char *sel_bg;
|
||||
char *sel_fg;
|
||||
char *css;
|
||||
guint i;
|
||||
|
||||
if (!screen)
|
||||
return;
|
||||
|
||||
lookup = gtk_style_context_new ();
|
||||
for (i = 0; theme_gtk3_providers_base && i < theme_gtk3_providers_base->len; i++)
|
||||
gtk_style_context_add_provider (lookup,
|
||||
GTK_STYLE_PROVIDER (g_ptr_array_index (theme_gtk3_providers_base, i)),
|
||||
GTK_STYLE_PROVIDER_PRIORITY_USER);
|
||||
for (i = 0; theme_gtk3_providers_variant && i < theme_gtk3_providers_variant->len; i++)
|
||||
gtk_style_context_add_provider (lookup,
|
||||
GTK_STYLE_PROVIDER (g_ptr_array_index (theme_gtk3_providers_variant, i)),
|
||||
GTK_STYLE_PROVIDER_PRIORITY_USER);
|
||||
|
||||
if (theme_gtk3_theme_colors)
|
||||
g_hash_table_remove_all (theme_gtk3_theme_colors);
|
||||
|
||||
bg = theme_gtk3_chrome_resolve_color (lookup, "theme_bg_color",
|
||||
prefer_dark ? "#242424" : "#f6f5f4");
|
||||
fg = theme_gtk3_chrome_resolve_color (lookup, "theme_fg_color",
|
||||
prefer_dark ? "#eeeeee" : "#2e3436");
|
||||
sel_bg = theme_gtk3_chrome_resolve_color (lookup, "theme_selected_bg_color", "#3584e4");
|
||||
sel_fg = theme_gtk3_chrome_resolve_color (lookup, "theme_selected_fg_color", "#ffffff");
|
||||
/* Not used by the chrome CSS itself, but resolved and stored for the
|
||||
* mapped-palette sampling fallback (chat area and friends). */
|
||||
g_free (theme_gtk3_chrome_resolve_color (lookup, "theme_base_color",
|
||||
prefer_dark ? "#1e1e1e" : "#ffffff"));
|
||||
g_object_unref (lookup);
|
||||
|
||||
css = g_strdup_printf (theme_gtk3_chrome_css_template,
|
||||
bg, fg,
|
||||
bg, fg);
|
||||
|
||||
if (!theme_gtk3_chrome_provider)
|
||||
theme_gtk3_chrome_provider = gtk_css_provider_new ();
|
||||
gtk_css_provider_load_from_data (theme_gtk3_chrome_provider, css, -1, NULL);
|
||||
|
||||
gtk_style_context_add_provider_for_screen (screen,
|
||||
GTK_STYLE_PROVIDER (theme_gtk3_chrome_provider),
|
||||
THEME_GTK3_CHROME_PRIORITY);
|
||||
|
||||
theme_gtk3_menu_uniform_apply (bg, fg, sel_bg, sel_fg);
|
||||
|
||||
g_free (css);
|
||||
g_free (sel_fg);
|
||||
g_free (sel_bg);
|
||||
g_free (fg);
|
||||
g_free (bg);
|
||||
}
|
||||
|
||||
static GtkCssProvider *theme_gtk3_menu_uniform_provider;
|
||||
|
||||
static void
|
||||
theme_gtk3_menu_uniform_apply (const char *bg, const char *fg, const char *sel_bg, const char *sel_fg)
|
||||
{
|
||||
GdkScreen *screen = gdk_screen_get_default ();
|
||||
char *css;
|
||||
|
||||
if (!screen)
|
||||
return;
|
||||
|
||||
css = g_strdup_printf (theme_gtk3_menu_uniform_template, bg, fg, sel_bg, sel_fg, fg);
|
||||
if (!theme_gtk3_menu_uniform_provider)
|
||||
theme_gtk3_menu_uniform_provider = gtk_css_provider_new ();
|
||||
gtk_css_provider_load_from_data (theme_gtk3_menu_uniform_provider, css, -1, NULL);
|
||||
gtk_style_context_add_provider_for_screen (screen,
|
||||
GTK_STYLE_PROVIDER (theme_gtk3_menu_uniform_provider),
|
||||
THEME_GTK3_MENU_UNIFORM_PRIORITY);
|
||||
g_free (css);
|
||||
}
|
||||
|
||||
static void
|
||||
theme_gtk3_chrome_provider_remove (void)
|
||||
{
|
||||
GdkScreen *screen = gdk_screen_get_default ();
|
||||
|
||||
if (!screen)
|
||||
return;
|
||||
|
||||
if (theme_gtk3_chrome_provider)
|
||||
gtk_style_context_remove_provider_for_screen (screen,
|
||||
GTK_STYLE_PROVIDER (theme_gtk3_chrome_provider));
|
||||
if (theme_gtk3_menu_uniform_provider)
|
||||
gtk_style_context_remove_provider_for_screen (screen,
|
||||
GTK_STYLE_PROVIDER (theme_gtk3_menu_uniform_provider));
|
||||
}
|
||||
|
||||
static void
|
||||
theme_gtk3_remove_provider (void)
|
||||
{
|
||||
GdkScreen *screen = gdk_screen_get_default ();
|
||||
guint i;
|
||||
|
||||
theme_gtk3_chrome_provider_remove ();
|
||||
if (screen && theme_gtk3_providers_variant)
|
||||
{
|
||||
for (i = 0; i < theme_gtk3_providers_variant->len; i++)
|
||||
@@ -839,6 +1095,7 @@ load_css_with_variant (ZoitechatGtk3Theme *theme, ThemeGtk3Variant variant, GErr
|
||||
}
|
||||
|
||||
g_ptr_array_unref (chain);
|
||||
theme_gtk3_chrome_provider_apply (prefer_dark);
|
||||
settings_apply_for_variant (variant);
|
||||
theme_gtk3_reset_widgets ();
|
||||
theme_gtk3_active = TRUE;
|
||||
@@ -849,24 +1106,35 @@ static gboolean
|
||||
theme_gtk3_apply_internal (const char *theme_id, ThemeGtk3Variant variant, gboolean force_reload, GError **error)
|
||||
{
|
||||
ZoitechatGtk3Theme *theme;
|
||||
char *previous_id = g_strdup (theme_gtk3_current_id);
|
||||
ThemeGtk3Variant previous_variant = theme_gtk3_current_variant;
|
||||
gboolean had_previous = theme_gtk3_active && previous_id && previous_id[0];
|
||||
char *previous_id;
|
||||
ThemeGtk3Variant previous_variant;
|
||||
gboolean had_previous;
|
||||
gboolean ok;
|
||||
|
||||
theme = zoitechat_gtk3_theme_find_by_id (theme_id);
|
||||
if (!theme)
|
||||
return g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_NOENT, "GTK3 theme not found."), FALSE;
|
||||
|
||||
/* An explicitly selected in-app theme pins the rendering to that
|
||||
* theme: it must not flip with the OS light/dark preference. Legacy
|
||||
* configs still carry FOLLOW_SYSTEM (the pre-inference default), so
|
||||
* resolve it to the variant the theme itself provides. */
|
||||
if (variant == THEME_GTK3_VARIANT_FOLLOW_SYSTEM)
|
||||
variant = theme_gtk3_infer_variant (theme);
|
||||
|
||||
if (!force_reload &&
|
||||
theme_gtk3_active &&
|
||||
g_strcmp0 (theme_gtk3_current_id, theme_id) == 0 &&
|
||||
theme_gtk3_current_variant == variant)
|
||||
return TRUE;
|
||||
|
||||
theme = zoitechat_gtk3_theme_find_by_id (theme_id);
|
||||
if (!theme)
|
||||
{
|
||||
g_free (previous_id);
|
||||
return g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_NOENT, "GTK3 theme not found."), FALSE;
|
||||
zoitechat_gtk3_theme_free (theme);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
previous_id = g_strdup (theme_gtk3_current_id);
|
||||
previous_variant = theme_gtk3_current_variant;
|
||||
had_previous = theme_gtk3_active && previous_id && previous_id[0];
|
||||
|
||||
theme_gtk3_remove_provider ();
|
||||
if (force_reload)
|
||||
theme_gtk3_invalidate_provider_cache ();
|
||||
@@ -934,6 +1202,12 @@ void
|
||||
theme_gtk3_disable (void)
|
||||
{
|
||||
theme_gtk3_remove_provider ();
|
||||
g_clear_object (&theme_gtk3_chrome_provider);
|
||||
g_clear_pointer (&theme_gtk3_theme_colors, g_hash_table_destroy);
|
||||
/* Following the system theme: keep the menu bar body and the menu
|
||||
* items on one surface too, using the system's own colors. */
|
||||
theme_gtk3_menu_uniform_apply ("@theme_bg_color", "@theme_fg_color",
|
||||
"@theme_selected_bg_color", "@theme_selected_fg_color");
|
||||
g_clear_pointer (&theme_gtk3_current_id, g_free);
|
||||
theme_gtk3_invalidate_provider_cache ();
|
||||
g_clear_pointer (&theme_gtk3_provider_cache, g_hash_table_destroy);
|
||||
@@ -963,3 +1237,13 @@ theme_gtk3_is_active (void)
|
||||
{
|
||||
return theme_gtk3_active;
|
||||
}
|
||||
|
||||
ThemeGtk3Variant
|
||||
theme_gtk3_active_variant (void)
|
||||
{
|
||||
/* Always resolved to PREFER_LIGHT or PREFER_DARK while a theme is
|
||||
* active; FOLLOW_SYSTEM only when no in-app theme is applied. */
|
||||
if (!theme_gtk3_active)
|
||||
return THEME_GTK3_VARIANT_FOLLOW_SYSTEM;
|
||||
return theme_gtk3_current_variant;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#define ZOITECHAT_THEME_GTK3_H
|
||||
|
||||
#include <glib.h>
|
||||
#include <gdk/gdk.h>
|
||||
|
||||
typedef enum
|
||||
{
|
||||
@@ -38,5 +39,7 @@ ThemeGtk3Variant theme_gtk3_variant_for_theme (const char *theme_id);
|
||||
void theme_gtk3_invalidate_provider_cache (void);
|
||||
void theme_gtk3_disable (void);
|
||||
gboolean theme_gtk3_is_active (void);
|
||||
ThemeGtk3Variant theme_gtk3_active_variant (void);
|
||||
gboolean theme_gtk3_lookup_theme_color (const char *name, GdkRGBA *out_color);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -58,10 +58,52 @@ typedef struct
|
||||
gboolean resolved_dark_preference;
|
||||
char gtk3_theme_id[sizeof prefs.hex_gui_gtk3_theme];
|
||||
int gtk3_variant;
|
||||
char *gtk_theme_name;
|
||||
} ThemeManagerAutoRefreshCache;
|
||||
|
||||
static ThemeManagerAutoRefreshCache theme_manager_auto_refresh_cache;
|
||||
|
||||
static void
|
||||
theme_manager_auto_refresh_cache_reset (void)
|
||||
{
|
||||
g_free (theme_manager_auto_refresh_cache.gtk_theme_name);
|
||||
memset (&theme_manager_auto_refresh_cache, 0, sizeof (theme_manager_auto_refresh_cache));
|
||||
}
|
||||
|
||||
static void
|
||||
theme_manager_auto_refresh_cache_store (gboolean resolved_dark_preference, char *gtk_theme_name)
|
||||
{
|
||||
theme_manager_auto_refresh_cache.initialized = TRUE;
|
||||
theme_manager_auto_refresh_cache.resolved_dark_preference = resolved_dark_preference;
|
||||
theme_manager_auto_refresh_cache.gtk3_variant = prefs.hex_gui_gtk3_variant;
|
||||
g_strlcpy (theme_manager_auto_refresh_cache.gtk3_theme_id,
|
||||
prefs.hex_gui_gtk3_theme,
|
||||
sizeof (theme_manager_auto_refresh_cache.gtk3_theme_id));
|
||||
g_free (theme_manager_auto_refresh_cache.gtk_theme_name);
|
||||
theme_manager_auto_refresh_cache.gtk_theme_name = gtk_theme_name;
|
||||
}
|
||||
|
||||
static char *
|
||||
theme_manager_dup_gtk_theme_name (void)
|
||||
{
|
||||
GtkSettings *settings = gtk_settings_get_default ();
|
||||
char *gtk_theme_name = NULL;
|
||||
|
||||
if (settings)
|
||||
g_object_get (settings, "gtk-theme-name", >k_theme_name, NULL);
|
||||
return gtk_theme_name;
|
||||
}
|
||||
|
||||
/* Record the state the application started up with so that the first queued
|
||||
* auto refresh only reacts to real changes; startup already applied the
|
||||
* current theme, and consumers such as the tray are not ready yet. */
|
||||
static void
|
||||
theme_manager_auto_refresh_cache_prime (void)
|
||||
{
|
||||
theme_manager_auto_refresh_cache_store (theme_policy_system_prefers_dark (),
|
||||
theme_manager_dup_gtk_theme_name ());
|
||||
}
|
||||
|
||||
static void theme_manager_apply_platform_window_theme (GtkWidget *window);
|
||||
|
||||
static void
|
||||
@@ -145,7 +187,9 @@ theme_manager_auto_dark_mode_changed (GtkSettings *settings, GParamSpec *pspec,
|
||||
gboolean color_change = FALSE;
|
||||
gboolean should_refresh_gtk3;
|
||||
gboolean gtk3_refresh;
|
||||
gboolean follows_system_theme;
|
||||
gboolean resolved_dark_preference;
|
||||
char *gtk_theme_name = NULL;
|
||||
static gboolean in_handler = FALSE;
|
||||
|
||||
(void) settings;
|
||||
@@ -154,39 +198,47 @@ theme_manager_auto_dark_mode_changed (GtkSettings *settings, GParamSpec *pspec,
|
||||
|
||||
resolved_dark_preference = theme_policy_system_prefers_dark ();
|
||||
gtk3_refresh = theme_manager_should_refresh_gtk3 ();
|
||||
follows_system_theme = prefs.hex_gui_gtk3_theme[0] == '\0';
|
||||
should_refresh_gtk3 = gtk3_refresh || prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_AUTO;
|
||||
|
||||
gtk_theme_name = theme_manager_dup_gtk_theme_name ();
|
||||
|
||||
if (theme_manager_auto_refresh_cache.initialized &&
|
||||
theme_manager_auto_refresh_cache.resolved_dark_preference == resolved_dark_preference &&
|
||||
theme_manager_auto_refresh_cache.gtk3_variant == prefs.hex_gui_gtk3_variant &&
|
||||
g_strcmp0 (theme_manager_auto_refresh_cache.gtk3_theme_id, prefs.hex_gui_gtk3_theme) == 0)
|
||||
g_strcmp0 (theme_manager_auto_refresh_cache.gtk3_theme_id, prefs.hex_gui_gtk3_theme) == 0 &&
|
||||
g_strcmp0 (theme_manager_auto_refresh_cache.gtk_theme_name, gtk_theme_name) == 0)
|
||||
{
|
||||
g_free (gtk_theme_name);
|
||||
return;
|
||||
}
|
||||
|
||||
theme_manager_auto_refresh_cache.initialized = TRUE;
|
||||
theme_manager_auto_refresh_cache.resolved_dark_preference = resolved_dark_preference;
|
||||
theme_manager_auto_refresh_cache.gtk3_variant = prefs.hex_gui_gtk3_variant;
|
||||
g_strlcpy (theme_manager_auto_refresh_cache.gtk3_theme_id,
|
||||
prefs.hex_gui_gtk3_theme,
|
||||
sizeof (theme_manager_auto_refresh_cache.gtk3_theme_id));
|
||||
theme_manager_auto_refresh_cache_store (resolved_dark_preference, gtk_theme_name);
|
||||
|
||||
if (prefs.hex_gui_dark_mode != ZOITECHAT_DARK_MODE_AUTO && !gtk3_refresh)
|
||||
if (prefs.hex_gui_dark_mode != ZOITECHAT_DARK_MODE_AUTO && !gtk3_refresh && !follows_system_theme)
|
||||
return;
|
||||
if (in_handler)
|
||||
return;
|
||||
|
||||
in_handler = TRUE;
|
||||
|
||||
if (prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_AUTO)
|
||||
if (prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_AUTO || follows_system_theme)
|
||||
{
|
||||
fe_set_auto_dark_mode_state (resolved_dark_preference);
|
||||
theme_manager_commit_preferences (prefs.hex_gui_dark_mode, &color_change);
|
||||
if (color_change)
|
||||
theme_manager_dispatch_changed (THEME_CHANGED_REASON_PALETTE | THEME_CHANGED_REASON_WIDGET_STYLE | THEME_CHANGED_REASON_USERLIST | THEME_CHANGED_REASON_MODE);
|
||||
if (prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_AUTO)
|
||||
fe_set_auto_dark_mode_state (resolved_dark_preference);
|
||||
}
|
||||
|
||||
if (should_refresh_gtk3)
|
||||
theme_gtk3_apply_current (NULL);
|
||||
|
||||
if (prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_AUTO || follows_system_theme)
|
||||
{
|
||||
/* The system theme feeds the mapped palette (chat area, user
|
||||
* list, channel tree), so consumers must re-resolve colors. */
|
||||
theme_manager_dispatch_changed (THEME_CHANGED_REASON_PALETTE | THEME_CHANGED_REASON_WIDGET_STYLE | THEME_CHANGED_REASON_USERLIST | THEME_CHANGED_REASON_MODE);
|
||||
}
|
||||
|
||||
in_handler = FALSE;
|
||||
}
|
||||
|
||||
@@ -204,18 +256,28 @@ theme_manager_run_auto_refresh (gpointer data)
|
||||
static void
|
||||
theme_manager_queue_auto_refresh (GtkSettings *settings, GParamSpec *pspec, gpointer data)
|
||||
{
|
||||
guint source_id;
|
||||
|
||||
(void) settings;
|
||||
(void) pspec;
|
||||
|
||||
if (theme_manager_auto_refresh_source != 0)
|
||||
return;
|
||||
|
||||
theme_manager_auto_refresh_source = theme_manager_idle_add_func (theme_manager_run_auto_refresh, data);
|
||||
theme_manager_auto_refresh_source = 1;
|
||||
source_id = theme_manager_idle_add_func (theme_manager_run_auto_refresh, data);
|
||||
/* An injected idle-add may run the refresh synchronously and already
|
||||
* have cleared the pending marker; only keep the id while pending. */
|
||||
if (theme_manager_auto_refresh_source != 0)
|
||||
theme_manager_auto_refresh_source = source_id;
|
||||
}
|
||||
|
||||
void
|
||||
theme_manager_init (void)
|
||||
{
|
||||
static gboolean settings_signals_connected = FALSE;
|
||||
GtkSettings *settings;
|
||||
|
||||
if (!theme_manager_listeners)
|
||||
theme_manager_listeners = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL,
|
||||
theme_listener_free);
|
||||
@@ -223,10 +285,25 @@ theme_manager_init (void)
|
||||
if (!theme_manager_setup_listener_id)
|
||||
theme_manager_setup_listener_id = theme_listener_register ("setup.apply", theme_manager_setup_apply_listener, NULL);
|
||||
|
||||
fe_set_auto_dark_mode_state (FALSE);
|
||||
theme_policy_init ();
|
||||
theme_policy_set_appearance_changed_callback (theme_manager_refresh_auto_mode);
|
||||
|
||||
settings = gtk_settings_get_default ();
|
||||
if (settings && !settings_signals_connected)
|
||||
{
|
||||
g_signal_connect (settings, "notify::gtk-theme-name",
|
||||
G_CALLBACK (theme_manager_queue_auto_refresh), NULL);
|
||||
g_signal_connect (settings, "notify::gtk-application-prefer-dark-theme",
|
||||
G_CALLBACK (theme_manager_queue_auto_refresh), NULL);
|
||||
settings_signals_connected = TRUE;
|
||||
}
|
||||
|
||||
fe_set_auto_dark_mode_state (prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_AUTO ?
|
||||
theme_policy_system_prefers_dark () : FALSE);
|
||||
theme_application_apply_mode (prefs.hex_gui_dark_mode, NULL);
|
||||
theme_gtk3_init ();
|
||||
zoitechat_set_theme_post_apply_callback (theme_manager_handle_theme_applied);
|
||||
theme_manager_auto_refresh_cache_prime ();
|
||||
}
|
||||
|
||||
gboolean
|
||||
@@ -513,13 +590,9 @@ theme_manager_apply_platform_window_theme (GtkWidget *window)
|
||||
|
||||
context = gtk_widget_get_style_context (window);
|
||||
if (theme_gtk3_is_active ())
|
||||
{
|
||||
dark = prefs.hex_gui_gtk3_variant == THEME_GTK3_VARIANT_PREFER_DARK;
|
||||
if (prefs.hex_gui_gtk3_variant == THEME_GTK3_VARIANT_FOLLOW_SYSTEM)
|
||||
dark = theme_policy_system_prefers_dark ();
|
||||
}
|
||||
dark = theme_gtk3_active_variant () == THEME_GTK3_VARIANT_PREFER_DARK;
|
||||
else
|
||||
dark = theme_runtime_is_dark_active ();
|
||||
dark = theme_policy_is_dark_mode_active (prefs.hex_gui_dark_mode);
|
||||
if (context)
|
||||
{
|
||||
gtk_style_context_remove_class (context, "zoitechat-dark");
|
||||
@@ -712,4 +785,5 @@ theme_manager_set_idle_add_func (ThemeManagerIdleAddFunc idle_add_func)
|
||||
{
|
||||
theme_manager_idle_add_func = idle_add_func ? idle_add_func : g_idle_add;
|
||||
theme_manager_auto_refresh_source = 0;
|
||||
theme_manager_auto_refresh_cache_reset ();
|
||||
}
|
||||
|
||||
@@ -20,14 +20,379 @@
|
||||
|
||||
#include "theme-policy.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
#include <gio/gio.h>
|
||||
|
||||
#include "theme-gtk3.h"
|
||||
#include "../fe-gtk.h"
|
||||
#include "../../common/zoitechat.h"
|
||||
#include "../../common/zoitechatc.h"
|
||||
|
||||
/* org.freedesktop.appearance color-scheme values (XDG desktop portal) */
|
||||
enum
|
||||
{
|
||||
THEME_POLICY_COLOR_SCHEME_NO_PREFERENCE = 0,
|
||||
THEME_POLICY_COLOR_SCHEME_PREFER_DARK = 1,
|
||||
THEME_POLICY_COLOR_SCHEME_PREFER_LIGHT = 2
|
||||
};
|
||||
|
||||
static ThemePolicyAppearanceChangedFunc policy_appearance_changed_cb;
|
||||
|
||||
/* GtkSettings state captured before ZoiteChat writes to it, so that values
|
||||
* coming from the user's own GTK configuration are not mistaken for values
|
||||
* this application set. */
|
||||
static gboolean policy_initial_gtk_state_captured;
|
||||
static gboolean policy_initial_prefer_dark;
|
||||
static char *policy_initial_theme_name;
|
||||
|
||||
#ifndef G_OS_WIN32
|
||||
static gboolean policy_portal_watch_started;
|
||||
static gboolean policy_portal_value_valid;
|
||||
static guint32 policy_portal_color_scheme;
|
||||
|
||||
static gboolean policy_gsettings_checked;
|
||||
static GSettings *policy_gsettings_interface;
|
||||
#endif
|
||||
|
||||
static void
|
||||
policy_capture_initial_gtk_state (void)
|
||||
{
|
||||
GtkSettings *settings;
|
||||
|
||||
if (policy_initial_gtk_state_captured)
|
||||
return;
|
||||
|
||||
settings = gtk_settings_get_default ();
|
||||
if (!settings)
|
||||
return;
|
||||
|
||||
g_object_get (settings,
|
||||
"gtk-application-prefer-dark-theme", &policy_initial_prefer_dark,
|
||||
"gtk-theme-name", &policy_initial_theme_name,
|
||||
NULL);
|
||||
policy_initial_gtk_state_captured = TRUE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
policy_theme_name_is_dark (const char *name)
|
||||
{
|
||||
char *lower;
|
||||
gboolean dark;
|
||||
|
||||
if (!name || !name[0])
|
||||
return FALSE;
|
||||
|
||||
lower = g_ascii_strdown (name, -1);
|
||||
dark = strstr (lower, "dark") != NULL || strstr (lower, "noir") != NULL ||
|
||||
strstr (lower, "black") != NULL;
|
||||
g_free (lower);
|
||||
return dark;
|
||||
}
|
||||
|
||||
#ifndef G_OS_WIN32
|
||||
|
||||
static void
|
||||
policy_notify_appearance_changed (void)
|
||||
{
|
||||
if (policy_appearance_changed_cb)
|
||||
policy_appearance_changed_cb ();
|
||||
}
|
||||
|
||||
static gboolean
|
||||
policy_color_scheme_from_variant (GVariant *value, guint32 *color_scheme)
|
||||
{
|
||||
GVariant *inner;
|
||||
gboolean ok = FALSE;
|
||||
|
||||
if (!value || !color_scheme)
|
||||
return FALSE;
|
||||
|
||||
inner = g_variant_ref (value);
|
||||
while (g_variant_is_of_type (inner, G_VARIANT_TYPE_VARIANT))
|
||||
{
|
||||
GVariant *unwrapped = g_variant_get_variant (inner);
|
||||
|
||||
g_variant_unref (inner);
|
||||
inner = unwrapped;
|
||||
}
|
||||
|
||||
if (g_variant_is_of_type (inner, G_VARIANT_TYPE_UINT32))
|
||||
{
|
||||
*color_scheme = g_variant_get_uint32 (inner);
|
||||
ok = TRUE;
|
||||
}
|
||||
|
||||
g_variant_unref (inner);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static void
|
||||
policy_portal_store_color_scheme (guint32 color_scheme)
|
||||
{
|
||||
gboolean changed;
|
||||
|
||||
changed = !policy_portal_value_valid || policy_portal_color_scheme != color_scheme;
|
||||
policy_portal_value_valid = TRUE;
|
||||
policy_portal_color_scheme = color_scheme;
|
||||
|
||||
if (changed)
|
||||
policy_notify_appearance_changed ();
|
||||
}
|
||||
|
||||
static void
|
||||
policy_portal_setting_changed_cb (GDBusConnection *connection, const char *sender_name,
|
||||
const char *object_path, const char *interface_name,
|
||||
const char *signal_name, GVariant *parameters,
|
||||
gpointer user_data)
|
||||
{
|
||||
const char *setting_namespace = NULL;
|
||||
const char *setting_key = NULL;
|
||||
GVariant *value = NULL;
|
||||
guint32 color_scheme = THEME_POLICY_COLOR_SCHEME_NO_PREFERENCE;
|
||||
|
||||
(void) connection;
|
||||
(void) sender_name;
|
||||
(void) object_path;
|
||||
(void) interface_name;
|
||||
(void) signal_name;
|
||||
(void) user_data;
|
||||
|
||||
if (!parameters || !g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(ssv)")))
|
||||
return;
|
||||
|
||||
g_variant_get (parameters, "(&s&sv)", &setting_namespace, &setting_key, &value);
|
||||
if (g_strcmp0 (setting_namespace, "org.freedesktop.appearance") == 0 &&
|
||||
g_strcmp0 (setting_key, "color-scheme") == 0 &&
|
||||
policy_color_scheme_from_variant (value, &color_scheme))
|
||||
policy_portal_store_color_scheme (color_scheme);
|
||||
|
||||
if (value)
|
||||
g_variant_unref (value);
|
||||
}
|
||||
|
||||
static void
|
||||
policy_portal_read_ready_cb (GObject *source, GAsyncResult *result, gpointer user_data)
|
||||
{
|
||||
GVariant *reply;
|
||||
GVariant *value = NULL;
|
||||
guint32 color_scheme = THEME_POLICY_COLOR_SCHEME_NO_PREFERENCE;
|
||||
|
||||
(void) user_data;
|
||||
|
||||
reply = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), result, NULL);
|
||||
if (!reply)
|
||||
return;
|
||||
|
||||
if (g_variant_is_of_type (reply, G_VARIANT_TYPE ("(v)")))
|
||||
{
|
||||
g_variant_get (reply, "(v)", &value);
|
||||
if (policy_color_scheme_from_variant (value, &color_scheme))
|
||||
policy_portal_store_color_scheme (color_scheme);
|
||||
if (value)
|
||||
g_variant_unref (value);
|
||||
}
|
||||
|
||||
g_variant_unref (reply);
|
||||
}
|
||||
|
||||
static void
|
||||
policy_portal_bus_ready_cb (GObject *source, GAsyncResult *result, gpointer user_data)
|
||||
{
|
||||
GDBusConnection *connection;
|
||||
|
||||
(void) source;
|
||||
(void) user_data;
|
||||
|
||||
connection = g_bus_get_finish (result, NULL);
|
||||
if (!connection)
|
||||
return;
|
||||
|
||||
g_dbus_connection_signal_subscribe (connection,
|
||||
"org.freedesktop.portal.Desktop",
|
||||
"org.freedesktop.portal.Settings",
|
||||
"SettingChanged",
|
||||
"/org/freedesktop/portal/desktop",
|
||||
NULL,
|
||||
G_DBUS_SIGNAL_FLAGS_NONE,
|
||||
policy_portal_setting_changed_cb,
|
||||
NULL,
|
||||
NULL);
|
||||
|
||||
g_dbus_connection_call (connection,
|
||||
"org.freedesktop.portal.Desktop",
|
||||
"/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.Settings",
|
||||
"Read",
|
||||
g_variant_new ("(ss)", "org.freedesktop.appearance", "color-scheme"),
|
||||
G_VARIANT_TYPE ("(v)"),
|
||||
G_DBUS_CALL_FLAGS_NONE,
|
||||
3000,
|
||||
NULL,
|
||||
policy_portal_read_ready_cb,
|
||||
NULL);
|
||||
g_object_unref (connection);
|
||||
}
|
||||
|
||||
static void
|
||||
policy_portal_watch_start (void)
|
||||
{
|
||||
if (policy_portal_watch_started)
|
||||
return;
|
||||
|
||||
policy_portal_watch_started = TRUE;
|
||||
g_bus_get (G_BUS_TYPE_SESSION, NULL, policy_portal_bus_ready_cb, NULL);
|
||||
}
|
||||
|
||||
static void
|
||||
policy_gsettings_changed_cb (GSettings *settings, const char *key, gpointer user_data)
|
||||
{
|
||||
(void) settings;
|
||||
(void) key;
|
||||
(void) user_data;
|
||||
|
||||
policy_notify_appearance_changed ();
|
||||
}
|
||||
|
||||
static GSettings *
|
||||
policy_get_interface_gsettings (void)
|
||||
{
|
||||
GSettingsSchemaSource *source;
|
||||
GSettingsSchema *schema;
|
||||
gboolean has_key;
|
||||
|
||||
if (policy_gsettings_checked)
|
||||
return policy_gsettings_interface;
|
||||
|
||||
policy_gsettings_checked = TRUE;
|
||||
|
||||
source = g_settings_schema_source_get_default ();
|
||||
if (!source)
|
||||
return NULL;
|
||||
|
||||
schema = g_settings_schema_source_lookup (source, "org.gnome.desktop.interface", TRUE);
|
||||
if (!schema)
|
||||
return NULL;
|
||||
|
||||
/* Requires GLib 2.40, always satisfied by the GTK 3.22 dependency. */
|
||||
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
|
||||
has_key = g_settings_schema_has_key (schema, "color-scheme");
|
||||
G_GNUC_END_IGNORE_DEPRECATIONS
|
||||
g_settings_schema_unref (schema);
|
||||
if (!has_key)
|
||||
return NULL;
|
||||
|
||||
policy_gsettings_interface = g_settings_new ("org.gnome.desktop.interface");
|
||||
g_signal_connect (policy_gsettings_interface, "changed::color-scheme",
|
||||
G_CALLBACK (policy_gsettings_changed_cb), NULL);
|
||||
return policy_gsettings_interface;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
policy_gsettings_color_scheme (gboolean *prefers_dark)
|
||||
{
|
||||
GSettings *settings;
|
||||
char *scheme;
|
||||
gboolean explicit_preference = FALSE;
|
||||
|
||||
settings = policy_get_interface_gsettings ();
|
||||
if (!settings)
|
||||
return FALSE;
|
||||
|
||||
scheme = g_settings_get_string (settings, "color-scheme");
|
||||
if (g_strcmp0 (scheme, "prefer-dark") == 0)
|
||||
{
|
||||
*prefers_dark = TRUE;
|
||||
explicit_preference = TRUE;
|
||||
}
|
||||
else if (g_strcmp0 (scheme, "prefer-light") == 0)
|
||||
{
|
||||
*prefers_dark = FALSE;
|
||||
explicit_preference = TRUE;
|
||||
}
|
||||
g_free (scheme);
|
||||
return explicit_preference;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
policy_gtk_heuristic_prefers_dark (void)
|
||||
{
|
||||
GtkSettings *settings;
|
||||
char *theme_name = NULL;
|
||||
gboolean dark;
|
||||
|
||||
policy_capture_initial_gtk_state ();
|
||||
|
||||
/* gtk-application-prefer-dark-theme is overwritten by ZoiteChat at
|
||||
* runtime, so only its captured startup value (settings.ini, XSETTINGS)
|
||||
* reflects the user's own configuration. */
|
||||
if (policy_initial_gtk_state_captured && policy_initial_prefer_dark)
|
||||
return TRUE;
|
||||
|
||||
if (theme_gtk3_is_active ())
|
||||
{
|
||||
/* gtk-theme-name currently holds the in-app theme override;
|
||||
* fall back to the captured system theme name. */
|
||||
theme_name = g_strdup (policy_initial_theme_name);
|
||||
}
|
||||
else
|
||||
{
|
||||
settings = gtk_settings_get_default ();
|
||||
if (settings)
|
||||
g_object_get (settings, "gtk-theme-name", &theme_name, NULL);
|
||||
}
|
||||
|
||||
dark = policy_theme_name_is_dark (theme_name);
|
||||
g_free (theme_name);
|
||||
return dark;
|
||||
}
|
||||
|
||||
#endif /* !G_OS_WIN32 */
|
||||
|
||||
void
|
||||
theme_policy_init (void)
|
||||
{
|
||||
policy_capture_initial_gtk_state ();
|
||||
#ifndef G_OS_WIN32
|
||||
policy_portal_watch_start ();
|
||||
#endif
|
||||
}
|
||||
|
||||
void
|
||||
theme_policy_set_appearance_changed_callback (ThemePolicyAppearanceChangedFunc callback)
|
||||
{
|
||||
policy_appearance_changed_cb = callback;
|
||||
}
|
||||
|
||||
gboolean
|
||||
theme_policy_system_prefers_dark (void)
|
||||
{
|
||||
return FALSE;
|
||||
#ifdef G_OS_WIN32
|
||||
gboolean prefer_dark = FALSE;
|
||||
|
||||
policy_capture_initial_gtk_state ();
|
||||
|
||||
if (fe_win32_high_contrast_is_enabled ())
|
||||
return FALSE;
|
||||
if (fe_win32_try_get_system_dark (&prefer_dark))
|
||||
return prefer_dark;
|
||||
return policy_theme_name_is_dark (policy_initial_theme_name);
|
||||
#else
|
||||
gboolean prefers_dark = FALSE;
|
||||
|
||||
policy_capture_initial_gtk_state ();
|
||||
policy_portal_watch_start ();
|
||||
|
||||
if (policy_portal_value_valid &&
|
||||
policy_portal_color_scheme != THEME_POLICY_COLOR_SCHEME_NO_PREFERENCE)
|
||||
return policy_portal_color_scheme == THEME_POLICY_COLOR_SCHEME_PREFER_DARK;
|
||||
|
||||
if (policy_gsettings_color_scheme (&prefers_dark))
|
||||
return prefers_dark;
|
||||
|
||||
return policy_gtk_heuristic_prefers_dark ();
|
||||
#endif
|
||||
}
|
||||
|
||||
gboolean
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
|
||||
#include "../fe-gtk.h"
|
||||
|
||||
typedef void (*ThemePolicyAppearanceChangedFunc) (void);
|
||||
|
||||
void theme_policy_init (void);
|
||||
void theme_policy_set_appearance_changed_callback (ThemePolicyAppearanceChangedFunc callback);
|
||||
gboolean theme_policy_system_prefers_dark (void);
|
||||
gboolean theme_policy_is_dark_mode_active (unsigned int mode);
|
||||
gboolean theme_policy_is_app_dark_mode_active (void);
|
||||
|
||||
@@ -35,8 +35,10 @@
|
||||
#include "theme-gtk3.h"
|
||||
#include "theme-manager.h"
|
||||
#include "theme-preferences.h"
|
||||
#include "theme-runtime.h"
|
||||
|
||||
extern void load_text_events (void);
|
||||
extern char **text_color_event_names (int color, int *count);
|
||||
|
||||
typedef struct
|
||||
{
|
||||
@@ -99,6 +101,9 @@ typedef struct
|
||||
gboolean changed;
|
||||
gboolean snapshot_valid[THEME_TOKEN_COUNT];
|
||||
gboolean staged_valid[THEME_TOKEN_COUNT];
|
||||
/* Only explicitly edited tokens are written back to the runtime
|
||||
* palette; untouched tokens keep following the GTK3 theme. */
|
||||
gboolean staged_dirty[THEME_TOKEN_COUNT];
|
||||
GdkRGBA snapshot[THEME_TOKEN_COUNT];
|
||||
GdkRGBA staged[THEME_TOKEN_COUNT];
|
||||
} theme_preferences_stage_state;
|
||||
@@ -145,7 +150,8 @@ theme_preferences_stage_sync_runtime_to_snapshot (void)
|
||||
|
||||
for (token = THEME_TOKEN_MIRC_0; token < THEME_TOKEN_COUNT; token++)
|
||||
{
|
||||
if (theme_preferences_stage.snapshot_valid[token])
|
||||
if (theme_preferences_stage.snapshot_valid[token] &&
|
||||
theme_preferences_stage.staged_dirty[token])
|
||||
theme_manager_set_token_color (ZOITECHAT_DARK_MODE_LIGHT, token,
|
||||
&theme_preferences_stage.snapshot[token], NULL);
|
||||
}
|
||||
@@ -158,7 +164,8 @@ theme_preferences_stage_sync_runtime_to_staged (void)
|
||||
|
||||
for (token = THEME_TOKEN_MIRC_0; token < THEME_TOKEN_COUNT; token++)
|
||||
{
|
||||
if (theme_preferences_stage.staged_valid[token])
|
||||
if (theme_preferences_stage.staged_valid[token] &&
|
||||
theme_preferences_stage.staged_dirty[token])
|
||||
theme_manager_set_token_color (ZOITECHAT_DARK_MODE_LIGHT, token,
|
||||
&theme_preferences_stage.staged[token], NULL);
|
||||
}
|
||||
@@ -177,6 +184,7 @@ theme_preferences_staged_set_color (ThemeSemanticToken token, const GdkRGBA *rgb
|
||||
{
|
||||
theme_preferences_stage.staged[token] = *rgba;
|
||||
theme_preferences_stage.staged_valid[token] = TRUE;
|
||||
theme_preferences_stage.staged_dirty[token] = TRUE;
|
||||
theme_preferences_stage_recompute_changed ();
|
||||
if (color_change_flag)
|
||||
*color_change_flag = theme_preferences_stage.changed;
|
||||
@@ -242,6 +250,9 @@ theme_preferences_stage_discard (void)
|
||||
static void
|
||||
theme_preferences_show_import_error (GtkWidget *button, const char *message);
|
||||
|
||||
static void
|
||||
theme_preferences_color_set_tooltip (GtkWidget *widget, ThemeSemanticToken token);
|
||||
|
||||
static void
|
||||
theme_preferences_manager_row_free (gpointer data)
|
||||
{
|
||||
@@ -480,6 +491,7 @@ theme_preferences_color_response_cb (GtkDialog *dialog, gint response_id, gpoint
|
||||
data->color_change_flag,
|
||||
TRUE);
|
||||
theme_preferences_color_button_apply (data->button, &rgba);
|
||||
theme_preferences_color_set_tooltip (data->button, data->token);
|
||||
theme_preferences_manager_update_preview ((theme_color_manager_ui *) data->manager_ui);
|
||||
}
|
||||
|
||||
@@ -558,6 +570,113 @@ theme_preferences_token_display_name (ThemeSemanticToken token)
|
||||
}
|
||||
}
|
||||
|
||||
static char *
|
||||
theme_preferences_color_tooltip_markup (ThemeSemanticToken token)
|
||||
{
|
||||
GString *tip;
|
||||
GdkRGBA rgba;
|
||||
char *display;
|
||||
char *hex;
|
||||
char *escaped;
|
||||
char **names;
|
||||
int count = 0;
|
||||
|
||||
if (token < THEME_TOKEN_MIRC_0 || token > THEME_TOKEN_MIRC_31)
|
||||
return NULL;
|
||||
|
||||
if (!theme_preferences_staged_get_color (token, &rgba))
|
||||
return NULL;
|
||||
|
||||
tip = g_string_new (NULL);
|
||||
|
||||
display = theme_preferences_token_display_name (token);
|
||||
hex = theme_preferences_format_hex (&rgba);
|
||||
escaped = g_markup_escape_text (display, -1);
|
||||
g_string_append_printf (tip,
|
||||
"<span background='%s'>\xe2\x80\x83\xe2\x80\x83</span> <b>%s</b> <tt><small>%s</small></tt>",
|
||||
hex, escaped, hex);
|
||||
g_free (escaped);
|
||||
g_free (display);
|
||||
|
||||
names = text_color_event_names (token - THEME_TOKEN_MIRC_0, &count);
|
||||
if (names)
|
||||
{
|
||||
int line_target;
|
||||
int line_len = 0;
|
||||
int i;
|
||||
|
||||
display = g_strdup_printf (ngettext ("Used by %d text event",
|
||||
"Used by %d text events", count), count);
|
||||
escaped = g_markup_escape_text (display, -1);
|
||||
g_string_append_printf (tip,
|
||||
"\n<small><span alpha='60%%'>%s</span></small>\n\n<small>",
|
||||
escaped);
|
||||
g_free (escaped);
|
||||
g_free (display);
|
||||
|
||||
/* Explicit line breaks keep the tooltip geometry stable;
|
||||
* heavily-used colors get wider lines so the full list
|
||||
* stays on screen. */
|
||||
if (count <= 12)
|
||||
line_target = 44;
|
||||
else if (count <= 40)
|
||||
line_target = 60;
|
||||
else
|
||||
line_target = 76;
|
||||
|
||||
for (i = 0; i < count; i++)
|
||||
{
|
||||
int name_len = (int) g_utf8_strlen (names[i], -1);
|
||||
|
||||
if (i)
|
||||
{
|
||||
if (line_len + name_len + 5 > line_target)
|
||||
{
|
||||
g_string_append_c (tip, '\n');
|
||||
line_len = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
g_string_append (tip, " \xc2\xb7 ");
|
||||
line_len += 5;
|
||||
}
|
||||
}
|
||||
escaped = g_markup_escape_text (names[i], -1);
|
||||
g_string_append (tip, escaped);
|
||||
line_len += name_len;
|
||||
g_free (escaped);
|
||||
}
|
||||
g_string_append (tip, "</small>");
|
||||
g_strfreev (names);
|
||||
}
|
||||
else
|
||||
{
|
||||
escaped = g_markup_escape_text (_("Not used by any text events"), -1);
|
||||
g_string_append_printf (tip,
|
||||
"\n<small><span alpha='60%%'><i>%s</i></span></small>",
|
||||
escaped);
|
||||
g_free (escaped);
|
||||
}
|
||||
|
||||
g_free (hex);
|
||||
return g_string_free (tip, FALSE);
|
||||
}
|
||||
|
||||
/* Static markup tooltips only: building tooltip widgets from a
|
||||
* query-tooltip handler makes GtkTooltip flicker on some setups, so the
|
||||
* markup is set once and refreshed whenever the color changes. */
|
||||
static void
|
||||
theme_preferences_color_set_tooltip (GtkWidget *widget, ThemeSemanticToken token)
|
||||
{
|
||||
char *markup = theme_preferences_color_tooltip_markup (token);
|
||||
|
||||
if (!markup)
|
||||
return;
|
||||
|
||||
gtk_widget_set_tooltip_markup (widget, markup);
|
||||
g_free (markup);
|
||||
}
|
||||
|
||||
static void
|
||||
theme_preferences_manager_row_apply (theme_color_manager_row *row, const GdkRGBA *rgba)
|
||||
{
|
||||
@@ -568,6 +687,7 @@ theme_preferences_manager_row_apply (theme_color_manager_row *row, const GdkRGBA
|
||||
hex = theme_preferences_format_hex (rgba);
|
||||
gtk_entry_set_text (GTK_ENTRY (row->entry), hex);
|
||||
g_free (hex);
|
||||
theme_preferences_color_set_tooltip (row->row, row->token);
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -727,6 +847,10 @@ theme_preferences_manager_dialog_response_cb (GtkDialog *dialog, gint response_i
|
||||
ThemeSemanticToken token;
|
||||
ThemeWidgetStyleValues style_values;
|
||||
|
||||
/* The runtime palette was just reset; drop any
|
||||
* pending edits so the reset is not re-pinned. */
|
||||
memset (theme_preferences_stage.staged_dirty, 0,
|
||||
sizeof (theme_preferences_stage.staged_dirty));
|
||||
for (token = THEME_TOKEN_MIRC_0; token < THEME_TOKEN_COUNT; token++)
|
||||
{
|
||||
GdkRGBA rgba;
|
||||
@@ -741,7 +865,6 @@ theme_preferences_manager_dialog_response_cb (GtkDialog *dialog, gint response_i
|
||||
theme_preferences_stage.staged_valid[THEME_TOKEN_TEXT_FOREGROUND] = TRUE;
|
||||
theme_preferences_stage.staged[THEME_TOKEN_TEXT_BACKGROUND] = style_values.background;
|
||||
theme_preferences_stage.staged_valid[THEME_TOKEN_TEXT_BACKGROUND] = TRUE;
|
||||
theme_preferences_stage_sync_runtime_to_staged ();
|
||||
theme_preferences_stage_recompute_changed ();
|
||||
if (ui->color_change_flag)
|
||||
*ui->color_change_flag = theme_preferences_stage.changed;
|
||||
@@ -883,6 +1006,8 @@ theme_preferences_create_color_manager_dialog (GtkWindow *parent, gboolean *colo
|
||||
if (theme_preferences_staged_get_color (token, &rgba))
|
||||
theme_preferences_manager_row_apply (row, &rgba);
|
||||
|
||||
theme_preferences_color_set_tooltip (list_row, token);
|
||||
|
||||
g_signal_connect (G_OBJECT (button), "clicked",
|
||||
G_CALLBACK (theme_preferences_manager_pick_cb), row);
|
||||
g_object_set_data (G_OBJECT (button), "zoitechat-theme-color-manager-ui", ui);
|
||||
@@ -1197,6 +1322,7 @@ theme_preferences_create_color_button (GtkWidget *table,
|
||||
g_object_set_data (G_OBJECT (but), "zoitechat-color-box", box);
|
||||
g_object_set_data (G_OBJECT (but), "zoitechat-theme-token", GINT_TO_POINTER (token));
|
||||
g_object_set_data (G_OBJECT (but), "zoitechat-theme-color-change", color_change_flag);
|
||||
theme_preferences_color_set_tooltip (but, token);
|
||||
gtk_grid_attach (GTK_GRID (table), but, col, row, 1, 1);
|
||||
g_signal_connect (G_OBJECT (but), "clicked", G_CALLBACK (theme_preferences_color_cb), parent);
|
||||
if (theme_preferences_staged_get_color (token, &color))
|
||||
@@ -1417,17 +1543,69 @@ theme_preferences_gtk3_sync_remove_state (theme_preferences_ui *ui)
|
||||
gtk_widget_set_sensitive (ui->gtk3_remove, source == ZOITECHAT_GTK3_THEME_SOURCE_USER);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
theme_preferences_gtk3_apply_and_refresh (GError **error)
|
||||
static void
|
||||
theme_preferences_stage_rebase_color (ThemeSemanticToken token, const GdkRGBA *rgba)
|
||||
{
|
||||
if (!theme_gtk3_apply_current (error))
|
||||
return FALSE;
|
||||
theme_manager_dispatch_changed (THEME_CHANGED_REASON_THEME_PACK |
|
||||
THEME_CHANGED_REASON_PALETTE |
|
||||
THEME_CHANGED_REASON_WIDGET_STYLE |
|
||||
THEME_CHANGED_REASON_USERLIST |
|
||||
THEME_CHANGED_REASON_MODE);
|
||||
return TRUE;
|
||||
if (token < 0 || token >= THEME_TOKEN_COUNT || !rgba)
|
||||
return;
|
||||
if (!theme_preferences_stage.active)
|
||||
return;
|
||||
|
||||
/* The theme changed underneath the dialog: shift the baseline so the
|
||||
* new theme-provided value is neither treated as a user edit nor
|
||||
* pinned into the runtime palette. */
|
||||
theme_preferences_stage.snapshot[token] = *rgba;
|
||||
theme_preferences_stage.staged[token] = *rgba;
|
||||
theme_preferences_stage.snapshot_valid[token] = TRUE;
|
||||
theme_preferences_stage.staged_valid[token] = TRUE;
|
||||
theme_preferences_stage.staged_dirty[token] = FALSE;
|
||||
theme_preferences_stage_recompute_changed ();
|
||||
}
|
||||
|
||||
static void
|
||||
theme_preferences_gtk3_sync_runtime_palette (theme_preferences_ui *ui)
|
||||
{
|
||||
ThemeWidgetStyleValues style_values;
|
||||
GtkWidget *style_source = NULL;
|
||||
|
||||
if (ui && ui->parent)
|
||||
style_source = GTK_WIDGET (ui->parent);
|
||||
else if (ui && ui->gtk3_combo)
|
||||
style_source = ui->gtk3_combo;
|
||||
|
||||
theme_runtime_clear_gtk_mapped_custom_tokens ();
|
||||
|
||||
theme_get_widget_style_values_for_widget (style_source, &style_values);
|
||||
|
||||
/* Leave the mapped tokens unpinned so the user list, channel tree and
|
||||
* input box keep resolving their colors from whatever GTK3 theme is
|
||||
* active (system or in-app selected), and only refresh what the
|
||||
* dialog shows. Pinning theme-derived values here freezes them into
|
||||
* the runtime palette (and colors.conf), which left those widgets
|
||||
* with stale colors until the next restart. */
|
||||
theme_preferences_stage_rebase_color (THEME_TOKEN_TEXT_FOREGROUND, &style_values.foreground);
|
||||
theme_preferences_stage_rebase_color (THEME_TOKEN_TEXT_BACKGROUND, &style_values.background);
|
||||
theme_preferences_stage_rebase_color (THEME_TOKEN_SELECTION_FOREGROUND, &style_values.selection_foreground);
|
||||
theme_preferences_stage_rebase_color (THEME_TOKEN_SELECTION_BACKGROUND, &style_values.selection_background);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
theme_preferences_gtk3_apply_and_refresh (theme_preferences_ui *ui, GError **error)
|
||||
{
|
||||
if (!theme_gtk3_apply_current (error))
|
||||
return FALSE;
|
||||
|
||||
if (ui && ui->parent)
|
||||
theme_manager_apply_to_window (GTK_WIDGET (ui->parent));
|
||||
|
||||
theme_preferences_gtk3_sync_runtime_palette (ui);
|
||||
|
||||
theme_manager_dispatch_changed (THEME_CHANGED_REASON_THEME_PACK |
|
||||
THEME_CHANGED_REASON_PALETTE |
|
||||
THEME_CHANGED_REASON_WIDGET_STYLE |
|
||||
THEME_CHANGED_REASON_USERLIST |
|
||||
THEME_CHANGED_REASON_MODE);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -1463,7 +1641,7 @@ theme_preferences_gtk3_changed_cb (GtkComboBox *combo, gpointer user_data)
|
||||
ui->setup_prefs->hex_gui_gtk3_variant = prefs.hex_gui_gtk3_variant;
|
||||
}
|
||||
|
||||
if (selection_changed && !theme_preferences_gtk3_apply_and_refresh (&error))
|
||||
if (selection_changed && !theme_preferences_gtk3_apply_and_refresh (ui, &error))
|
||||
{
|
||||
theme_preferences_show_message (ui, GTK_MESSAGE_ERROR,
|
||||
error ? error->message : _("Failed to apply GTK3 theme."));
|
||||
@@ -1552,7 +1730,7 @@ theme_preferences_populate_gtk3 (theme_preferences_ui *ui)
|
||||
g_free (final_id);
|
||||
}
|
||||
|
||||
if (should_apply && !theme_preferences_gtk3_apply_and_refresh (&error))
|
||||
if (should_apply && !theme_preferences_gtk3_apply_and_refresh (ui, &error))
|
||||
{
|
||||
theme_preferences_show_message (ui, GTK_MESSAGE_ERROR,
|
||||
error ? error->message : _("Failed to apply GTK3 theme."));
|
||||
|
||||
@@ -410,6 +410,20 @@ theme_runtime_reset_mode_colors (gboolean dark_mode)
|
||||
dark_mode_active = FALSE;
|
||||
}
|
||||
|
||||
void
|
||||
theme_runtime_clear_gtk_mapped_custom_tokens (void)
|
||||
{
|
||||
light_custom_tokens[THEME_TOKEN_TEXT_FOREGROUND] = FALSE;
|
||||
light_custom_tokens[THEME_TOKEN_TEXT_BACKGROUND] = FALSE;
|
||||
light_custom_tokens[THEME_TOKEN_SELECTION_FOREGROUND] = FALSE;
|
||||
light_custom_tokens[THEME_TOKEN_SELECTION_BACKGROUND] = FALSE;
|
||||
|
||||
dark_custom_tokens[THEME_TOKEN_TEXT_FOREGROUND] = FALSE;
|
||||
dark_custom_tokens[THEME_TOKEN_TEXT_BACKGROUND] = FALSE;
|
||||
dark_custom_tokens[THEME_TOKEN_SELECTION_FOREGROUND] = FALSE;
|
||||
dark_custom_tokens[THEME_TOKEN_SELECTION_BACKGROUND] = FALSE;
|
||||
}
|
||||
|
||||
void
|
||||
theme_runtime_load (void)
|
||||
{
|
||||
|
||||
@@ -47,6 +47,7 @@ gboolean theme_runtime_apply_dark_mode (gboolean enable);
|
||||
void theme_runtime_user_set_color (ThemeSemanticToken token, const GdkRGBA *col);
|
||||
void theme_runtime_dark_set_color (ThemeSemanticToken token, const GdkRGBA *col);
|
||||
void theme_runtime_reset_mode_colors (gboolean dark_mode);
|
||||
void theme_runtime_clear_gtk_mapped_custom_tokens (void);
|
||||
gboolean theme_runtime_get_color (ThemeSemanticToken token, GdkRGBA *out_rgba);
|
||||
gboolean theme_runtime_mode_has_user_colors (gboolean dark_mode);
|
||||
void theme_runtime_get_widget_style_values (ThemeWidgetStyleValues *out_values);
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "fe-gtk.h"
|
||||
|
||||
@@ -53,6 +54,36 @@ enum
|
||||
|
||||
static void userlist_store_color (GtkListStore *store, GtkTreeIter *iter, ThemeSemanticToken token, gboolean has_token);
|
||||
|
||||
static const char *
|
||||
userlist_typing_suffix (session *sess, struct User *user)
|
||||
{
|
||||
static const char *active[] = { " [✎]", " [✎.]", " [✎..]" };
|
||||
|
||||
if (!user || !user->typing)
|
||||
return "";
|
||||
|
||||
if (user->typing == 2)
|
||||
return " [✎…]";
|
||||
|
||||
return active[sess->typing_animation_frame % G_N_ELEMENTS (active)];
|
||||
}
|
||||
|
||||
static char *
|
||||
userlist_nick_markup (session *sess, struct User *user)
|
||||
{
|
||||
char *nick = g_markup_escape_text (user->nick, -1);
|
||||
const char *typing = userlist_typing_suffix (sess, user);
|
||||
|
||||
if (*typing)
|
||||
{
|
||||
char *marked = g_strdup_printf ("%s%s", nick, typing);
|
||||
g_free (nick);
|
||||
return marked;
|
||||
}
|
||||
|
||||
return nick;
|
||||
}
|
||||
|
||||
static const char *
|
||||
userlist_prefix_color (char prefix)
|
||||
{
|
||||
@@ -463,6 +494,87 @@ fe_userlist_remove (session *sess, struct User *user)
|
||||
return sel;
|
||||
}
|
||||
|
||||
|
||||
static gboolean
|
||||
userlist_typing_tick (session *sess)
|
||||
{
|
||||
GtkTreeModel *model;
|
||||
GtkTreeIter iter;
|
||||
gboolean valid;
|
||||
gboolean keep = FALSE;
|
||||
time_t now = time (NULL);
|
||||
|
||||
if (!sess || !sess->res || !sess->res->user_model)
|
||||
return FALSE;
|
||||
|
||||
sess->typing_animation_frame++;
|
||||
model = GTK_TREE_MODEL (sess->res->user_model);
|
||||
valid = gtk_tree_model_get_iter_first (model, &iter);
|
||||
|
||||
while (valid)
|
||||
{
|
||||
struct User *user = NULL;
|
||||
|
||||
gtk_tree_model_get (model, &iter, COL_USER, &user, -1);
|
||||
if (user && user->typing)
|
||||
{
|
||||
char *nick;
|
||||
|
||||
if ((user->typing == 1 && now - user->typing_time >= 6) || (user->typing == 2 && now - user->typing_time >= 30))
|
||||
user->typing = 0;
|
||||
|
||||
nick = userlist_nick_markup (sess, user);
|
||||
gtk_list_store_set (sess->res->user_model, &iter, COL_NICK, nick, -1);
|
||||
g_free (nick);
|
||||
if (user->typing)
|
||||
keep = TRUE;
|
||||
}
|
||||
valid = gtk_tree_model_iter_next (model, &iter);
|
||||
}
|
||||
|
||||
if (!keep)
|
||||
{
|
||||
sess->typing_animation_tag = 0;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void
|
||||
fe_userlist_set_typing (session *sess, const char *nick, const char *state)
|
||||
{
|
||||
struct User *user;
|
||||
GtkTreeIter *iter;
|
||||
int sel;
|
||||
|
||||
if (!sess || !nick || !sess->res || !sess->res->user_model)
|
||||
return;
|
||||
|
||||
user = userlist_find (sess, nick);
|
||||
if (!user)
|
||||
return;
|
||||
|
||||
if (!strcmp (state, "active"))
|
||||
user->typing = 1;
|
||||
else if (!strcmp (state, "paused"))
|
||||
user->typing = 2;
|
||||
else
|
||||
user->typing = 0;
|
||||
user->typing_time = time (NULL);
|
||||
|
||||
iter = find_row (sess, GTK_TREE_VIEW (sess->gui->user_tree), GTK_TREE_MODEL (sess->res->user_model), user, &sel);
|
||||
if (iter)
|
||||
{
|
||||
char *nick = userlist_nick_markup (sess, user);
|
||||
gtk_list_store_set (sess->res->user_model, iter, COL_NICK, nick, -1);
|
||||
g_free (nick);
|
||||
}
|
||||
|
||||
if (user->typing && !sess->typing_animation_tag)
|
||||
sess->typing_animation_tag = fe_timeout_add (350, userlist_typing_tick, sess);
|
||||
}
|
||||
|
||||
void
|
||||
fe_userlist_rehash (session *sess, struct User *user)
|
||||
{
|
||||
@@ -493,9 +605,14 @@ fe_userlist_rehash (session *sess, struct User *user)
|
||||
}
|
||||
}
|
||||
|
||||
gtk_list_store_set (GTK_LIST_STORE (sess->res->user_model), iter,
|
||||
{
|
||||
char *nick = userlist_nick_markup (sess, user);
|
||||
gtk_list_store_set (GTK_LIST_STORE (sess->res->user_model), iter,
|
||||
COL_NICK, nick,
|
||||
COL_HOST, user->hostname,
|
||||
-1);
|
||||
g_free (nick);
|
||||
}
|
||||
userlist_store_color (GTK_LIST_STORE (sess->res->user_model), iter, nick_token, have_nick_token);
|
||||
}
|
||||
|
||||
@@ -506,7 +623,6 @@ fe_userlist_insert (session *sess, struct User *newuser, gboolean sel)
|
||||
GdkPixbuf *pix = get_user_icon (sess->server, newuser);
|
||||
GtkTreeIter iter;
|
||||
char *nick;
|
||||
char *nick_escaped;
|
||||
char *prefix = NULL;
|
||||
char *prefix_escaped;
|
||||
char prefix_text[2];
|
||||
@@ -530,8 +646,7 @@ fe_userlist_insert (session *sess, struct User *newuser, gboolean sel)
|
||||
}
|
||||
}
|
||||
|
||||
nick_escaped = g_markup_escape_text (newuser->nick, -1);
|
||||
nick = nick_escaped;
|
||||
nick = userlist_nick_markup (sess, newuser);
|
||||
if (!prefs.hex_gui_ulist_icons)
|
||||
{
|
||||
if (newuser->prefix[0] != '\0' && newuser->prefix[0] != ' ')
|
||||
@@ -559,7 +674,7 @@ fe_userlist_insert (session *sess, struct User *newuser, gboolean sel)
|
||||
userlist_store_color (GTK_LIST_STORE (model), &iter, nick_token, have_nick_token);
|
||||
|
||||
g_free (prefix);
|
||||
g_free (nick_escaped);
|
||||
g_free (nick);
|
||||
|
||||
userlist_row_map_set (sess, model, newuser, &iter);
|
||||
|
||||
@@ -757,6 +872,7 @@ userlist_add_columns (GtkTreeView * treeview)
|
||||
gtk_tree_view_column_pack_start (column, renderer, TRUE);
|
||||
gtk_tree_view_column_add_attribute (column, renderer, "markup", COL_NICK);
|
||||
gtk_tree_view_column_add_attribute (column, renderer, THEME_GTK_FOREGROUND_PROPERTY, COL_GDKCOLOR);
|
||||
|
||||
column = gtk_tree_view_get_column (GTK_TREE_VIEW (treeview), 1);
|
||||
gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_FIXED);
|
||||
gtk_tree_view_column_set_expand (column, TRUE);
|
||||
|
||||
@@ -26,6 +26,7 @@ GtkWidget *userlist_create (GtkWidget *box);
|
||||
GtkListStore *userlist_create_model (session *sess);
|
||||
void userlist_show (session *sess);
|
||||
void userlist_select (session *sess, char *name);
|
||||
void fe_userlist_set_typing (session *sess, const char *nick, const char *state);
|
||||
char **userlist_selection_list (GtkWidget *widget, int *num_ret);
|
||||
GdkPixbuf *get_user_icon (server *serv, struct User *user);
|
||||
|
||||
|
||||
@@ -286,10 +286,12 @@ xtext_adj_get_page_increment (GtkAdjustment *adj)
|
||||
static void
|
||||
xtext_adjustment_apply (GtkAdjustment *adj, gdouble lower, gdouble upper, gdouble value, gdouble page_size)
|
||||
{
|
||||
gtk_adjustment_set_lower (adj, lower);
|
||||
gtk_adjustment_set_upper (adj, upper);
|
||||
gtk_adjustment_set_page_size (adj, page_size);
|
||||
gtk_adjustment_set_value (adj, value);
|
||||
gdouble step_increment = page_size * prefs.hex_gui_mouse_scroll_speed / 100.0;
|
||||
|
||||
if (step_increment < 1)
|
||||
step_increment = 1;
|
||||
|
||||
gtk_adjustment_configure (adj, value, lower, upper, step_increment, page_size, page_size);
|
||||
}
|
||||
|
||||
static inline void
|
||||
|
||||
@@ -647,6 +647,11 @@ void
|
||||
fe_set_topic (struct session *sess, char *topic, char *stripped_topic)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
fe_set_typing (struct session *sess, const char *nick, const char *state)
|
||||
{
|
||||
}
|
||||
void
|
||||
fe_cleanup (void)
|
||||
{
|
||||
@@ -852,6 +857,10 @@ fe_get_str (char *prompt, char *def, void *callback, void *ud)
|
||||
{
|
||||
}
|
||||
void
|
||||
fe_get_password (char *prompt, void *callback, void *ud)
|
||||
{
|
||||
}
|
||||
void
|
||||
fe_get_int (char *prompt, int def, void *callback, void *ud)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
<None Include="$(DepsRoot)\bin\*zlib*.dll" />
|
||||
<None Include="$(DepsRoot)\bin\*girepository*.dll" Condition="'$(LuaEnabled)'=='true' and '$(LuaLibDir)'!=''" />
|
||||
<None Include="$(Python3Path)\Lib\site-packages\_cffi_backend.*.pyd" Condition="'$(Python3Enabled)'=='true'" />
|
||||
<None Include="$(Python38Path)\Lib\site-packages\_cffi_backend.*.pyd" Condition="'$(Python38Enabled)'=='true'" />
|
||||
|
||||
<LuaLib Include="$(LuaLibDir)\**\*.dll" Condition="'$(LuaEnabled)'=='true' and '$(LuaLibDir)'!=''" />
|
||||
<LuaShare Include="$(LuaShareDir)\*.lua" Condition="'$(LuaEnabled)'=='true' and '$(LuaShareDir)'!=''" />
|
||||
@@ -105,10 +106,10 @@
|
||||
<Copy SourceFiles="@(LuaShare)" DestinationFiles="@(LuaShare->'$(ZoiteChatRel)\share\lua\%(RecursiveDir)%(Filename)%(Extension)')" />
|
||||
<Copy SourceFiles="@(LuaLib)" DestinationFiles="@(LuaLib->'$(ZoiteChatRel)\lib\lua\%(RecursiveDir)%(Filename)%(Extension)')" />
|
||||
<Copy SourceFiles="@(Typelib)" DestinationFiles="@(Typelib->'$(ZoiteChatRel)\lib\girepository-1.0\%(Filename)%(Extension)')" />
|
||||
<Copy SourceFiles="..\..\plugins\python\xchat.py" DestinationFolder="$(ZoiteChatRel)\python" Condition="'$(Python3Enabled)'=='true'" />
|
||||
<Copy SourceFiles="..\..\plugins\python\hexchat.py" DestinationFolder="$(ZoiteChatRel)\python" Condition="'$(Python3Enabled)'=='true'" />
|
||||
<Copy SourceFiles="..\..\plugins\python\zoitechat.py" DestinationFolder="$(ZoiteChatRel)\python" Condition="'$(Python3Enabled)'=='true'" />
|
||||
<Copy SourceFiles="..\..\plugins\python\_zoitechat.py" DestinationFolder="$(ZoiteChatRel)\python" Condition="'$(Python3Enabled)'=='true'" />
|
||||
<Copy SourceFiles="..\..\plugins\python\xchat.py" DestinationFolder="$(ZoiteChatRel)\python" Condition="'$(Python3Enabled)'=='true' or '$(Python38Enabled)'=='true'" />
|
||||
<Copy SourceFiles="..\..\plugins\python\hexchat.py" DestinationFolder="$(ZoiteChatRel)\python" Condition="'$(Python3Enabled)'=='true' or '$(Python38Enabled)'=='true'" />
|
||||
<Copy SourceFiles="..\..\plugins\python\zoitechat.py" DestinationFolder="$(ZoiteChatRel)\python" Condition="'$(Python3Enabled)'=='true' or '$(Python38Enabled)'=='true'" />
|
||||
<Copy SourceFiles="..\..\plugins\python\_zoitechat.py" DestinationFolder="$(ZoiteChatRel)\python" Condition="'$(Python3Enabled)'=='true' or '$(Python38Enabled)'=='true'" />
|
||||
|
||||
<WriteLinesToFile File="$(ZoiteChatRel)portable-mode" Lines="2" Overwrite="true" />
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ AppUpdatesURL=http://zoitechat.org/downloads.html
|
||||
LicenseFile=share\doc\zoitechat\COPYING
|
||||
UninstallDisplayIcon={app}\zoitechat.exe
|
||||
UninstallDisplayName=ZoiteChat
|
||||
DefaultDirName={pf64}\ZoiteChat
|
||||
DefaultDirName={commonpf64}\ZoiteChat
|
||||
DisableDirPage=no
|
||||
DefaultGroupName=ZoiteChat
|
||||
AllowNoIcons=yes
|
||||
@@ -28,11 +28,11 @@ OutputBaseFilename={#APPNAM}-{#APPVER}_x64
|
||||
FlatComponentsList=no
|
||||
PrivilegesRequired=none
|
||||
ShowComponentSizes=no
|
||||
CreateUninstallRegKey=not IsTaskSelected('portable')
|
||||
Uninstallable=not IsTaskSelected('portable')
|
||||
CreateUninstallRegKey=not WizardIsTaskSelected('portable')
|
||||
Uninstallable=not WizardIsTaskSelected('portable')
|
||||
ArchitecturesAllowed=x64compatible
|
||||
ArchitecturesInstallIn64BitMode=x64compatible
|
||||
MinVersion=6.1
|
||||
MinVersion=6.1sp1
|
||||
WizardImageFile={#PROJECTDIR}wizardimage.bmp
|
||||
WizardSmallImageFile={#PROJECTDIR}wizardsmallimage.bmp
|
||||
SetupIconFile={#PROJECTDIR}..\..\data\icons\zoitechat.ico
|
||||
@@ -59,7 +59,7 @@ Name: "plugins\upd"; Description: "Update Checker"; Types: normal custom; Flags:
|
||||
Name: "langs"; Description: "Language Interfaces"; Types: custom; Flags: disablenouninstallwarning
|
||||
Name: "langs\lua"; Description: "Lua (LuaJIT 2.1)"; Types: normal custom; Flags: disablenouninstallwarning
|
||||
Name: "langs\perl"; Description: "Perl (Strawberry Perl 5.42.0.1)"; Types: custom; Flags: disablenouninstallwarning
|
||||
Name: "langs\python"; Description: "Python (Python 3.14.3)"; Types: custom; Flags: disablenouninstallwarning
|
||||
Name: "langs\python"; Description: "Python (Python 3.14.3, or 3.8.10 before Windows 10)"; Types: custom; Flags: disablenouninstallwarning
|
||||
Name: "themes"; Description: "GTK3 Themes"; Types: normal minimal custom; Flags: disablenouninstallwarning
|
||||
Name: "themes\windows10"; Description: "Windows 10"; Types: normal minimal custom; Flags: disablenouninstallwarning
|
||||
Name: "themes\windows10dark"; Description: "Windows 10 Dark"; Types: normal minimal custom; Flags: disablenouninstallwarning
|
||||
@@ -181,7 +181,8 @@ Source: "plugins\hcperl.dll"; DestDir: "{app}\plugins"; Flags: ignoreversion; Co
|
||||
|
||||
Source: "python\*.py"; DestDir: "{app}\python"; Flags: ignoreversion; Components: langs\python
|
||||
|
||||
Source: "plugins\hcpython3.dll"; DestDir: "{app}\plugins"; Flags: ignoreversion; Components: langs\python
|
||||
Source: "plugins\hcpython3.dll"; DestDir: "{app}\plugins"; Flags: ignoreversion; Components: langs\python; Check: not NeedsLegacyPython
|
||||
Source: "plugins\hcpython38.dll"; DestDir: "{app}\plugins"; Flags: ignoreversion skipifsourcedoesntexist; Components: langs\python; Check: NeedsLegacyPython
|
||||
Source: "_cffi_backend.cp3*.pyd"; DestDir: "{app}"; Flags: ignoreversion; Components: langs\python
|
||||
|
||||
Source: "zoitechat.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: libs
|
||||
@@ -328,6 +329,32 @@ begin
|
||||
Result := DirExists(ExpandConstant('{localappdata}') + '\enchant\myspell');
|
||||
end;
|
||||
|
||||
// Python >= 3.9 dropped support for Windows versions before 10;
|
||||
// those systems get Python 3.8 and the matching plugin instead.
|
||||
function NeedsLegacyPython(): Boolean;
|
||||
var
|
||||
Version: TWindowsVersion;
|
||||
begin
|
||||
GetWindowsVersionEx(Version);
|
||||
Result := Version.NTPlatform and (Version.Major < 10);
|
||||
end;
|
||||
|
||||
function PythonURL(): String;
|
||||
begin
|
||||
if NeedsLegacyPython() then
|
||||
Result := 'https://www.python.org/ftp/python/3.8.10/python-3.8.10-amd64.exe'
|
||||
else
|
||||
Result := 'https://www.python.org/ftp/python/3.14.3/python-3.14.3-amd64.exe';
|
||||
end;
|
||||
|
||||
function PythonDLL(): String;
|
||||
begin
|
||||
if NeedsLegacyPython() then
|
||||
Result := 'python38.dll'
|
||||
else
|
||||
Result := 'python314.dll';
|
||||
end;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// Sets up the automatic downloads
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
@@ -345,34 +372,34 @@ begin
|
||||
begin
|
||||
idpClearFiles;
|
||||
|
||||
if IsComponentSelected('themes\windows10') then
|
||||
if WizardIsComponentSelected('themes\windows10') then
|
||||
idpAddFile('https://dl.zoitechat.zoite.net/themes/GTK3Themes/Windows-10-3.2.1.zip', ExpandConstant('{tmp}\Windows-10-3.2.1.zip'));
|
||||
|
||||
if IsComponentSelected('themes\windows10dark') then
|
||||
if WizardIsComponentSelected('themes\windows10dark') then
|
||||
idpAddFile('https://dl.zoitechat.zoite.net/themes/GTK3Themes/Windows-10-Dark-3.2.1-dark.zip', ExpandConstant('{tmp}\Windows-10-Dark-3.2.1-dark.zip'));
|
||||
|
||||
if not IsTaskSelected('portable') then
|
||||
if not WizardIsTaskSelected('portable') then
|
||||
begin
|
||||
|
||||
REDIST := 'https://aka.ms/vs/17/release/vc_redist.x64.exe';
|
||||
PERL := 'https://github.com/StrawberryPerl/Perl-Dist-Strawberry/releases/download/SP_54201_64bit/strawberry-perl-5.42.0.1-64bit.msi';
|
||||
PY3 := 'https://www.python.org/ftp/python/3.14.3/python-3.14.3-amd64.exe';
|
||||
SPELL := 'https://github.com/zoitechat/gvsbuild/releases/download/zoitechat-2.16.2/ZoiteChat.Spelling.Dictionaries.r2.exe';
|
||||
PY3 := PythonURL();
|
||||
SPELL := 'https://github.com/hexchat/gvsbuild/releases/download/hexchat-2.16.2/HexChat.Spelling.Dictionaries.r2.exe';
|
||||
|
||||
if IsComponentSelected('deps\vcredist2015') and not CheckVCInstall() then
|
||||
if WizardIsComponentSelected('deps\vcredist2015') and not CheckVCInstall() then
|
||||
idpAddFile(REDIST, ExpandConstant('{tmp}\vcredist.exe'));
|
||||
|
||||
if IsComponentSelected('spell') and not CheckSpellInstall() then
|
||||
if WizardIsComponentSelected('spell') and not CheckSpellInstall() then
|
||||
idpAddFile(SPELL, ExpandConstant('{tmp}\spelling-dicts.exe'));
|
||||
|
||||
if not WizardSilent() then
|
||||
begin
|
||||
if IsComponentSelected('langs\perl') and not CheckDLL('perl542.dll') then
|
||||
if WizardIsComponentSelected('langs\perl') and not CheckDLL('perl542.dll') then
|
||||
begin
|
||||
idpAddFile(PERL, ExpandConstant('{tmp}\perl.msi'))
|
||||
end;
|
||||
|
||||
if IsComponentSelected('langs\python') and not CheckDLL('python314.dll') then
|
||||
if WizardIsComponentSelected('langs\python') and not CheckDLL(PythonDLL()) then
|
||||
idpAddFile(PY3, ExpandConstant('{tmp}\python.exe'));
|
||||
end;
|
||||
end;
|
||||
@@ -397,7 +424,7 @@ begin
|
||||
|
||||
if (CurPageID = wpSelectTasks) then
|
||||
if (WizardForm.TasksList.Checked[1] = True) then
|
||||
if (WizardDirValue() = ExpandConstant('{pf64}\ZoiteChat')) then
|
||||
if (WizardDirValue() = ExpandConstant('{commonpf64}\ZoiteChat')) then
|
||||
begin
|
||||
WizardForm.TasksList.Checked[1] := False
|
||||
MsgBox('Portable mode is only intended for use on portable drives and has been disabled.', mbInformation, MB_OK)
|
||||
@@ -405,21 +432,21 @@ begin
|
||||
|
||||
if CurPageID = wpReady then
|
||||
begin
|
||||
if IsComponentSelected('themes\windows10') and not FileExists(ExpandConstant('{tmp}\Windows-10-3.2.1.zip')) then
|
||||
if WizardIsComponentSelected('themes\windows10') and not FileExists(ExpandConstant('{tmp}\Windows-10-3.2.1.zip')) then
|
||||
begin
|
||||
MsgBox('Windows 10 GTK3 theme could not be downloaded. Please retry setup and rerun setup.', mbError, MB_OK);
|
||||
Result := False;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
if IsComponentSelected('themes\windows10dark') and not FileExists(ExpandConstant('{tmp}\Windows-10-Dark-3.2.1-dark.zip')) then
|
||||
if WizardIsComponentSelected('themes\windows10dark') and not FileExists(ExpandConstant('{tmp}\Windows-10-Dark-3.2.1-dark.zip')) then
|
||||
begin
|
||||
MsgBox('Windows 10 Dark GTK3 theme could not be downloaded. Please retry setup and rerun setup.', mbError, MB_OK);
|
||||
Result := False;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
if IsComponentSelected('deps\vcredist2015') and not CheckVCInstall() and not FileExists(ExpandConstant('{tmp}\vcredist.exe')) then
|
||||
if WizardIsComponentSelected('deps\vcredist2015') and not CheckVCInstall() and not FileExists(ExpandConstant('{tmp}\vcredist.exe')) then
|
||||
begin
|
||||
MsgBox('Visual C++ Redistributable could not be downloaded. Please retry setup or install it manually and rerun setup.', mbError, MB_OK);
|
||||
Result := False;
|
||||
@@ -486,7 +513,7 @@ end;
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
procedure CurStepChanged(CurStep: TSetupStep);
|
||||
begin
|
||||
if not (IsTaskSelected('portable')) then
|
||||
if not (WizardIsTaskSelected('portable')) then
|
||||
begin
|
||||
if (CurStep=ssInstall) then
|
||||
begin
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.18.1
|
||||
2.18.3
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<YourGendefPath>c:\gtk-build\gendef</YourGendefPath>
|
||||
<YourPerlPath>c:\gtk-build\perl-5.42.0.1</YourPerlPath>
|
||||
<YourPython3Path>c:\gtk-build\python-3.14</YourPython3Path>
|
||||
<YourPython38Path>c:\gtk-build\python-3.8</YourPython38Path>
|
||||
<YourWinSparklePath>c:\gtk-build\WinSparkle</YourWinSparklePath>
|
||||
|
||||
<!-- YOU SHOULDN'T TOUCH ANYTHING BELOW -->
|
||||
@@ -32,6 +33,12 @@
|
||||
<Python3Enabled Condition="Exists('$(Python3Path)\\python.exe') and Exists('$(Python3Path)\\libs\\$(Python3Lib).lib') and Exists('$(Python3Path)\\Lib\\site-packages\\cffi')">true</Python3Enabled>
|
||||
<Python3Enabled Condition="'$(Python3Enabled)'==''">false</Python3Enabled>
|
||||
|
||||
<Python38Path>$(YourPython38Path)\$(ZoiteChatPlatform)</Python38Path>
|
||||
<Python38Lib>python38</Python38Lib>
|
||||
<Python38Output>hcpython38</Python38Output>
|
||||
<Python38Enabled Condition="Exists('$(Python38Path)\\python.exe') and Exists('$(Python38Path)\\libs\\$(Python38Lib).lib') and Exists('$(Python38Path)\\Lib\\site-packages\\cffi')">true</Python38Enabled>
|
||||
<Python38Enabled Condition="'$(Python38Enabled)'==''">false</Python38Enabled>
|
||||
|
||||
<PerlEnabled Condition="Exists('$(PerlPath)\\bin\\perl.exe') and Exists('$(PerlPath)\\bin\\$(PerlLib).dll') and Exists('$(GendefPath)\\gendef.exe')">true</PerlEnabled>
|
||||
<PerlEnabled Condition="'$(PerlEnabled)'==''">false</PerlEnabled>
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "installer", "installer\installer.vcxproj", "{5A0F4962-E670-4DA2-9E45-52CC47F26E2F}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
{C2321A03-0BA7-45B3-8740-ABD82B36B0BF} = {C2321A03-0BA7-45B3-8740-ABD82B36B0BF}
|
||||
{D58924D2-1492-4AC3-B058-7A214424EEDC} = {D58924D2-1492-4AC3-B058-7A214424EEDC}
|
||||
{BF0EBC16-68AD-4CD1-864C-5B56836EBE2A} = {BF0EBC16-68AD-4CD1-864C-5B56836EBE2A}
|
||||
{17E4BE39-76F7-4A06-AD21-EFD0C5091F76} = {17E4BE39-76F7-4A06-AD21-EFD0C5091F76}
|
||||
{4C0F3940-2EEE-4646-82F7-6CE75B9A72F4} = {4C0F3940-2EEE-4646-82F7-6CE75B9A72F4}
|
||||
@@ -80,6 +81,11 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "perl", "..\plugins\perl\per
|
||||
{87554B59-006C-4D94-9714-897B27067BA3} = {87554B59-006C-4D94-9714-897B27067BA3}
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "python38", "..\plugins\python\python38.vcxproj", "{D58924D2-1492-4AC3-B058-7A214424EEDC}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
{87554B59-006C-4D94-9714-897B27067BA3} = {87554B59-006C-4D94-9714-897B27067BA3}
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "python3", "..\plugins\python\python3.vcxproj", "{C2321A03-0BA7-45B3-8740-ABD82B36B0BF}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
{87554B59-006C-4D94-9714-897B27067BA3} = {87554B59-006C-4D94-9714-897B27067BA3}
|
||||
@@ -127,6 +133,8 @@ Global
|
||||
{D90BC3E3-1341-4849-9354-5F40489D39D1}.Release|x64.Build.0 = Release|x64
|
||||
{C2321A03-0BA7-45B3-8740-ABD82B36B0BF}.Release|x64.ActiveCfg = Release|x64
|
||||
{C2321A03-0BA7-45B3-8740-ABD82B36B0BF}.Release|x64.Build.0 = Release|x64
|
||||
{D58924D2-1492-4AC3-B058-7A214424EEDC}.Release|x64.ActiveCfg = Release|x64
|
||||
{D58924D2-1492-4AC3-B058-7A214424EEDC}.Release|x64.Build.0 = Release|x64
|
||||
{C53145CC-D021-40C9-B97C-0249AB9A43C9}.Release|x64.ActiveCfg = Release|x64
|
||||
{C53145CC-D021-40C9-B97C-0249AB9A43C9}.Release|x64.Build.0 = Release|x64
|
||||
{BF0EBC16-68AD-4CD1-864C-5B56836EBE2A}.Release|x64.ActiveCfg = Release|x64
|
||||
@@ -151,6 +159,7 @@ Global
|
||||
{5A0F4962-E670-4DA2-9E45-52CC47F26E2F} = {0FD996A7-464F-4981-8380-3DCA3A244A13}
|
||||
{D90BC3E3-1341-4849-9354-5F40489D39D1} = {D237DA6B-BD5F-46C0-8BEA-50E9A1340240}
|
||||
{C2321A03-0BA7-45B3-8740-ABD82B36B0BF} = {D237DA6B-BD5F-46C0-8BEA-50E9A1340240}
|
||||
{D58924D2-1492-4AC3-B058-7A214424EEDC} = {D237DA6B-BD5F-46C0-8BEA-50E9A1340240}
|
||||
{C53145CC-D021-40C9-B97C-0249AB9A43C9} = {561126F4-FA18-45FC-A2BF-8F858F161D6D}
|
||||
{BF0EBC16-68AD-4CD1-864C-5B56836EBE2A} = {021EC1D0-FF67-4700-9AB2-EAABF1159C09}
|
||||
{4C0F3940-2EEE-4646-82F7-6CE75B9A72F4} = {D237DA6B-BD5F-46C0-8BEA-50E9A1340240}
|
||||
|
||||
Reference in New Issue
Block a user