mirror of
https://github.com/ZoiteChat/zoitechat.git
synced 2026-10-17 18:37:14 +00:00
Compare commits
43
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c630730a1d | ||
|
|
9126ce9202 | ||
|
|
d3f892e9a7 | ||
|
|
bf89691b3f | ||
|
|
5029a34fcb | ||
|
|
73ccded3ac | ||
|
|
44e677a973 | ||
|
|
92d39cd077 | ||
|
|
1cdee8ea6b | ||
|
|
e11604fd62 | ||
|
|
a2e8d9087e | ||
|
|
edf268dc0f | ||
|
|
e03ea59f3c | ||
|
|
f0b2840668 | ||
|
|
4f2fc49475 | ||
|
|
2e4155bbfc | ||
|
|
ca2456cbc6 | ||
|
|
222b4868f5 | ||
|
|
2cf1f7e833 | ||
|
|
776d24695e | ||
|
|
64a9c6e5d7 | ||
|
|
66196013bc | ||
|
|
61ff308c9f | ||
|
|
2672f9b087 | ||
|
|
4315a75a12 | ||
|
|
15905bebb1 | ||
|
|
fc4a7454ad | ||
|
|
87410aea50 | ||
|
|
de98d00a1c | ||
|
|
e94ffd9d79 | ||
|
|
d7aebb784d | ||
|
|
20b8cb939d | ||
|
|
ff4217c3d1 | ||
|
|
4fb72d30d8 | ||
|
|
d2b8fc0bda | ||
|
|
951181628a | ||
|
|
ccee274847 | ||
|
|
967f6728ef | ||
|
|
bf11ffb626 | ||
|
|
c12cabbe4e | ||
|
|
040021ee0e | ||
|
|
5018a3a848 | ||
|
|
df1c74a91d |
@@ -33,7 +33,7 @@ jobs:
|
||||
libgtk-3-dev libfontconfig1-dev libpango1.0-dev xvfb xauth dbus-daemon at-spi2-core \
|
||||
libwayland-client0 libwayland-cursor0 libwayland-egl1 \
|
||||
libxkbcommon0 \
|
||||
libgtk-3-bin libglib2.0-bin shared-mime-info gsettings-desktop-schemas \
|
||||
libgtk-3-bin libglib2.0-bin glib-networking shared-mime-info gsettings-desktop-schemas \
|
||||
liblua5.4-dev libpci-dev libperl-dev libssl-dev libayatana-appindicator3-dev \
|
||||
perl python3 python3-minimal python3-dev python3-cffi mono-devel desktop-file-utils \
|
||||
breeze-gtk-theme \
|
||||
@@ -135,6 +135,20 @@ jobs:
|
||||
cp -a /usr/lib/gtk-3.0/modules AppDir/usr/lib/gtk-3.0/
|
||||
fi
|
||||
|
||||
# HTTPS inline-image downloads use GSocketClient TLS on Linux.
|
||||
# GLib provides the API, but TLS and system-proxy support live in
|
||||
# loadable GIO modules shipped by glib-networking. Bundle those
|
||||
# modules so the AppImage does not depend on the host copy.
|
||||
gio_module_dir="$(pkg-config --variable=giomoduledir gio-2.0)"
|
||||
|
||||
test -n "$gio_module_dir"
|
||||
test -f "${gio_module_dir}/libgiognutls.so"
|
||||
install -d "AppDir${gio_module_dir}"
|
||||
cp -a "${gio_module_dir}"/libgio*.so "AppDir${gio_module_dir}/"
|
||||
gio-querymodules "AppDir${gio_module_dir}"
|
||||
test -f "AppDir${gio_module_dir}/libgiognutls.so"
|
||||
test -s "AppDir${gio_module_dir}/giomodule.cache"
|
||||
|
||||
- name: Verify bundled plugins
|
||||
run: |
|
||||
set -eux
|
||||
|
||||
@@ -2,6 +2,7 @@ appdir = join_paths(get_option('datadir'), 'applications')
|
||||
metainfodir = join_paths(get_option('datadir'), 'metainfo')
|
||||
mimedir = join_paths(get_option('datadir'), 'mime', 'packages')
|
||||
desktop_utils = find_program('desktop-file-validate', required: false)
|
||||
appstreamcli = find_program('appstreamcli', required: false)
|
||||
|
||||
if get_option('gtk-frontend')
|
||||
if get_option('install-appdata')
|
||||
@@ -13,7 +14,6 @@ if get_option('gtk-frontend')
|
||||
install_dir: metainfodir
|
||||
)
|
||||
|
||||
appstreamcli = find_program('appstreamcli', required: false)
|
||||
if appstreamcli.found()
|
||||
test('Validate net.zoite.Zoitechat.appdata.xml', appstreamcli,
|
||||
args: ['validate', zoitechat_appdata]
|
||||
|
||||
@@ -29,6 +29,22 @@
|
||||
<id>zoitechat.desktop</id>
|
||||
</provides>
|
||||
<releases>
|
||||
<release date="2026-09-15" version="2.19.1">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Fixed /msg messages showing twice on servers using IRCv3 <code>echo-message</code>.</li>
|
||||
<li>Fixed escaping a leading command slash so <code>//text</code> sends <code>/text</code> as normal chat.</li>
|
||||
<li>Fixed automatic away clearing when sending messages on servers using <code>echo-message</code>.</li>
|
||||
<li>Fixed other users' away status not updating until rejoining a channel when <code>away-notify</code> updates are missed.</li>
|
||||
<li>Fixed private messages and <code>/me</code> actions being routed to the wrong window when using WeeChat's IRC relay, while preserving channel ACTION routing.</li>
|
||||
<li>Fixed a Flatpak startup error caused by the D-Bus service still using the old HexChat app ID.</li>
|
||||
<li>Fixed optional Windows 10 theme downloads in the Windows installer.</li>
|
||||
<li>Fixed the GTK warning when switching from channel tabs to the channel tree.</li>
|
||||
<li>Fixed GTK3 theme archive root detection so themes with an <code>index.theme</code> file are preferred correctly.</li>
|
||||
<li>Fixed source-build configuration when plugin AppStream metadata installation is disabled, and corrected the minimum Meson version to 0.59.</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release date="2026-07-28" version="2.19.0">
|
||||
<description>
|
||||
<ul>
|
||||
|
||||
Vendored
+153
@@ -78,6 +78,11 @@ a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--accent);
|
||||
color: #fff8ea;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
textarea,
|
||||
@@ -139,6 +144,7 @@ select {
|
||||
z-index: 2000;
|
||||
border-bottom: 3px solid var(--rule);
|
||||
background: rgba(238, 229, 211, 0.94);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
@@ -231,6 +237,17 @@ select {
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.site-nav a[aria-current="page"] {
|
||||
border-block-color: var(--rule);
|
||||
background: var(--panel-dark);
|
||||
color: var(--paper-warm);
|
||||
box-shadow: inset 0 -0.3rem 0 var(--accent);
|
||||
}
|
||||
|
||||
.site-nav a[aria-current="page"]::before {
|
||||
color: #d19b25;
|
||||
}
|
||||
|
||||
.site-nav a:hover,
|
||||
.site-nav a:focus-visible {
|
||||
border-block-color: var(--rule);
|
||||
@@ -511,6 +528,7 @@ select {
|
||||
font-weight: 900;
|
||||
line-height: 1.18;
|
||||
box-shadow: 4px 4px 0 var(--rule);
|
||||
touch-action: manipulation;
|
||||
transition: transform 0.12s ease, background-color 0.12s ease, color 0.12s ease, box-shadow 0.12s ease;
|
||||
}
|
||||
|
||||
@@ -527,6 +545,11 @@ select {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.button:active {
|
||||
transform: translate(3px, 3px);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.button-primary {
|
||||
background: var(--accent);
|
||||
color: #fff8ea;
|
||||
@@ -1176,6 +1199,11 @@ select {
|
||||
color: rgba(255, 248, 234, 0.82);
|
||||
}
|
||||
|
||||
.footer-links a {
|
||||
width: fit-content;
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
|
||||
.footer-links a::before {
|
||||
color: #d19b25;
|
||||
content: "↳ ";
|
||||
@@ -1184,6 +1212,7 @@ select {
|
||||
.footer-links a:hover,
|
||||
.footer-links a:focus-visible {
|
||||
color: #fff8ea;
|
||||
border-bottom-color: currentColor;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -1346,6 +1375,70 @@ select {
|
||||
animation-iteration-count: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Automatic dark appearance, matching the main ZoiteChat site palette. */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--paper: #161514;
|
||||
--paper-warm: #f4ead9;
|
||||
--paper-cool: #1c2522;
|
||||
--ink: #f2eadb;
|
||||
--ink-soft: #ddd2c1;
|
||||
--muted: #bdb19f;
|
||||
--quiet: #928777;
|
||||
--rule: #080706;
|
||||
--rule-soft: rgba(242, 234, 219, 0.16);
|
||||
--rule-mid: rgba(242, 234, 219, 0.3);
|
||||
--panel: #211e1a;
|
||||
--panel-alt: #2b261f;
|
||||
--panel-dark: #0b0c0c;
|
||||
--accent: #c34720;
|
||||
--accent-dark: #ff966d;
|
||||
--accent-soft: rgba(195, 71, 32, 0.17);
|
||||
--green: #367662;
|
||||
--blue: #386b8d;
|
||||
--warning: #9b6508;
|
||||
--success: #43856a;
|
||||
--shadow: 8px 8px 0 rgba(0, 0, 0, 0.82);
|
||||
--shadow-soft: 0 18px 40px rgba(0, 0, 0, 0.36);
|
||||
}
|
||||
|
||||
html {
|
||||
background: var(--paper);
|
||||
}
|
||||
|
||||
body {
|
||||
background:
|
||||
linear-gradient(90deg, rgba(242, 234, 219, 0.025) 1px, transparent 1px) 0 0 / 68px 68px,
|
||||
linear-gradient(180deg, rgba(242, 234, 219, 0.022) 1px, transparent 1px) 0 0 / 68px 68px,
|
||||
radial-gradient(circle at 11% 18%, rgba(195, 71, 32, 0.15), transparent 24rem),
|
||||
radial-gradient(circle at 86% 4%, rgba(54, 118, 98, 0.16), transparent 28rem),
|
||||
linear-gradient(180deg, #181715 0%, #111311 58%, #191714 100%);
|
||||
}
|
||||
|
||||
body::before {
|
||||
background:
|
||||
repeating-linear-gradient(90deg, transparent 0 17px, rgba(242, 234, 219, 0.025) 17px 18px),
|
||||
repeating-linear-gradient(0deg, transparent 0 5px, rgba(255, 255, 255, 0.025) 5px 6px);
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
.site-header {
|
||||
background: rgba(22, 21, 20, 0.94);
|
||||
}
|
||||
|
||||
.site-nav a,
|
||||
.nav-menu summary {
|
||||
background: rgba(39, 35, 30, 0.52);
|
||||
}
|
||||
|
||||
.eyebrow::before,
|
||||
.footer-links a::before {
|
||||
color: #e0aa3c;
|
||||
}
|
||||
}
|
||||
|
||||
/* Sphinx/Read the Docs bridge: aggressively map documentation onto the main ZoiteChat site skin. */
|
||||
html {
|
||||
font-size: 16px !important;
|
||||
@@ -1485,6 +1578,17 @@ body a {
|
||||
font-weight: 900 !important;
|
||||
}
|
||||
|
||||
#navbar-top .site-nav a[aria-current="page"] {
|
||||
border-block-color: var(--rule) !important;
|
||||
background: var(--panel-dark) !important;
|
||||
color: var(--paper-warm) !important;
|
||||
box-shadow: inset 0 -0.3rem 0 var(--accent) !important;
|
||||
}
|
||||
|
||||
#navbar-top .site-nav a[aria-current="page"]::before {
|
||||
color: #d19b25 !important;
|
||||
}
|
||||
|
||||
#navbar-top .site-nav a:hover,
|
||||
#navbar-top .site-nav a:focus-visible,
|
||||
#navbar-top .site-nav summary:hover,
|
||||
@@ -1671,6 +1775,7 @@ footer.site-footer-lite {
|
||||
}
|
||||
|
||||
.document .body p {
|
||||
max-width: 78ch;
|
||||
margin: 0 0 1rem !important;
|
||||
}
|
||||
|
||||
@@ -1716,6 +1821,7 @@ ul.search a:focus-visible {
|
||||
|
||||
.document .body ul,
|
||||
.document .body ol {
|
||||
max-width: 78ch;
|
||||
margin: 0.75rem 0 1.4rem !important;
|
||||
padding-left: 1.35rem !important;
|
||||
}
|
||||
@@ -1865,6 +1971,11 @@ ul.search a:focus-visible {
|
||||
text-transform: uppercase !important;
|
||||
}
|
||||
|
||||
.document .body table tbody tr:hover td,
|
||||
.document .body table tbody tr:focus-within td {
|
||||
background: var(--accent-soft) !important;
|
||||
}
|
||||
|
||||
.admonition,
|
||||
.note,
|
||||
.warning,
|
||||
@@ -2004,6 +2115,7 @@ input[type="submit"],
|
||||
line-height: 1.18 !important;
|
||||
text-shadow: none !important;
|
||||
box-shadow: 4px 4px 0 var(--rule) !important;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.btn:hover,
|
||||
@@ -2019,6 +2131,13 @@ input[type="submit"]:focus-visible {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.btn:active,
|
||||
button:active,
|
||||
input[type="submit"]:active {
|
||||
transform: translate(3px, 3px) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
#navbar-related {
|
||||
min-height: auto !important;
|
||||
margin: 1rem 0 1.5rem !important;
|
||||
@@ -2275,3 +2394,37 @@ footer:not(.site-footer) {
|
||||
letter-spacing: -0.04em !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Keep basicstrap's stronger overrides readable with the main site's dark palette. */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
#navbar-top.site-header {
|
||||
background: rgba(22, 21, 20, 0.94) !important;
|
||||
}
|
||||
|
||||
#navbar-top .site-nav a,
|
||||
#navbar-top .site-nav summary {
|
||||
background: rgba(39, 35, 30, 0.52) !important;
|
||||
}
|
||||
|
||||
.document::after,
|
||||
.sidebar::after,
|
||||
#navbar-related::after {
|
||||
background: linear-gradient(135deg, transparent 50%, rgba(242, 234, 219, 0.09) 50%) !important;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="search"],
|
||||
.search-query,
|
||||
.form-control,
|
||||
textarea,
|
||||
select {
|
||||
background: var(--panel) !important;
|
||||
box-shadow: inset 3px 3px 0 rgba(0, 0, 0, 0.32) !important;
|
||||
}
|
||||
|
||||
.warning,
|
||||
.caution,
|
||||
.danger {
|
||||
background: #3a2e19 !important;
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+5
-1
@@ -209,6 +209,8 @@
|
||||
{%- endif %}
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
<meta name="theme-color" content="#eee5d3" media="(prefers-color-scheme: light)">
|
||||
<meta name="theme-color" content="#161514" media="(prefers-color-scheme: dark)">
|
||||
|
||||
{{ css() }}
|
||||
{%- if not embedded %}
|
||||
@@ -260,8 +262,9 @@
|
||||
<nav class="site-nav" aria-label="Primary">
|
||||
<a href="https://zoitechat.org/">Home</a>
|
||||
<a href="https://zoitechat.org/addons.php">Addons</a>
|
||||
<a href="https://zoitechat.org/announcements.php">Announcements</a>
|
||||
<a href="https://zoitechat.org/download.php">Download</a>
|
||||
<a href="{{ pathto(master_doc) }}">Docs</a>
|
||||
<a href="{{ pathto(master_doc) }}" aria-current="page">Docs</a>
|
||||
<a href="https://github.com/ZoiteChat/zoitechat" target="_blank" rel="noopener">GitHub</a>
|
||||
<details class="nav-menu">
|
||||
<summary>IRC Support</summary>
|
||||
@@ -336,6 +339,7 @@
|
||||
<div class="footer-links">
|
||||
<a href="https://zoitechat.org/download.php">Download</a>
|
||||
<a href="https://zoitechat.org/addons.php">Addons</a>
|
||||
<a href="https://zoitechat.org/announcements.php">Announcements</a>
|
||||
<a href="https://zoitechat.org/privacy.php">Privacy Policy</a>
|
||||
<a href="{{ pathto(master_doc) }}">Documentation</a>
|
||||
<a href="https://github.com/ZoiteChat/zoitechat" target="_blank" rel="noopener">Source code</a>
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
ZoiteChat ChangeLog
|
||||
=================
|
||||
|
||||
2.19.1 (2026-09-15)
|
||||
-------------------
|
||||
|
||||
- Fixed ``/msg`` messages showing twice on servers using IRCv3 ``echo-message``.
|
||||
- Fixed escaping a leading command slash so ``//text`` sends ``/text`` as normal chat.
|
||||
- Fixed automatic away clearing when sending messages on servers using ``echo-message``.
|
||||
- Fixed other users' away status not updating until rejoining a channel when ``away-notify`` updates are missed.
|
||||
- Fixed private messages and ``/me`` actions being routed to the wrong window when using WeeChat's IRC relay, while preserving channel ACTION routing.
|
||||
- Fixed a Flatpak startup error caused by the D-Bus service still using the old HexChat app ID.
|
||||
- Fixed optional Windows 10 theme downloads in the Windows installer.
|
||||
- Fixed the GTK warning when switching from channel tabs to the channel tree.
|
||||
- Fixed GTK3 theme archive root detection so themes with an ``index.theme`` file are preferred correctly.
|
||||
- Fixed source-build configuration when plugin AppStream metadata installation is disabled, and corrected the minimum Meson version to 0.59.
|
||||
|
||||
2.19.0 (2026-07-28)
|
||||
-------------------
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# Optional Python asyncio integration
|
||||
|
||||
`run_async(coroutine, context=None, interval=10)` returns an AsyncTask with
|
||||
`done()`, `cancel()` and `result()`. Each invocation owns a separate event loop
|
||||
pumped by an existing main-loop timer. A ready stop callback ensures selector
|
||||
polling never blocks the UI. The captured context is restored after every pump;
|
||||
a closed context cancels the work. No process-wide event-loop policy is changed.
|
||||
|
||||
Existing synchronous scripts are unaffected. Use `asyncio.get_running_loop()`
|
||||
inside the coroutine. Child tasks use the same loop/context and are cancelled
|
||||
when the main coroutine finishes or the script unloads. Cancellation cleanup is
|
||||
bounded; tasks must not suppress cancellation indefinitely. Cleanup uses the
|
||||
captured context while it remains open; after it closes, cancellation cleanup uses
|
||||
the current valid context. Use explicit Context methods for context-specific
|
||||
output in finally blocks, and do not issue context-dependent commands there. This provides normal
|
||||
asyncio sockets/timers, not Windows subprocess support or a new HTTP library.
|
||||
Asyncio hostname resolution can use its executor; use a resolver appropriate to
|
||||
your library if you require no worker threads. Blocking work inside a coroutine
|
||||
still blocks the client and must be avoided.
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import zoitechat
|
||||
|
||||
async def later():
|
||||
await asyncio.sleep(1)
|
||||
zoitechat.prnt('Still in the context where this task was started')
|
||||
|
||||
task = zoitechat.run_async(later())
|
||||
```
|
||||
@@ -0,0 +1,35 @@
|
||||
# Script-supported capabilities
|
||||
|
||||
`register_capability(name, userdata=None, connection_id=None)` returns a normal
|
||||
hook handle. Register before connecting/reconnecting. Core requests the name only
|
||||
if advertised in CAP LS or CAP NEW. Existing built-in requests remain enabled;
|
||||
SASL and STS registration is rejected so scripts cannot change authentication or
|
||||
transport policy through this API. None/-1 means any connection (zero is a valid
|
||||
connection ID). Names are lowercase ASCII alphanumerics plus - . / _, 1-200 bytes.
|
||||
Duplicate registrations do not duplicate requests for an advertised token.
|
||||
|
||||
`unhook(handle)` or script/plugin unload removes future request ownership. It
|
||||
intentionally does not disable a capability already acknowledged on a live
|
||||
connection, because another plugin/core may depend on it. Registering after CAP
|
||||
LS does not retrospectively negotiate an already-advertised capability: reconnect
|
||||
or wait for CAP NEW. Core retains ownership of CAP END; this API does not extend
|
||||
SASL waiting or delay registration awaiting plugin-only CAP ACKs.
|
||||
|
||||
`hook_capability(name, callback, userdata=None, priority=PRI_NORM)` independently
|
||||
observes negotiation through existing raw CAP hooks. Callback receives
|
||||
`(CapabilityEvent, userdata)` with name, subcommand, value, enabled, context and
|
||||
connection_id. LS/NEW mean advertised (`enabled=None`), ACK/LIST mean enabled
|
||||
(unless a negative ACK), NAK means rejected and DEL means removed. Callback
|
||||
returns are ignored so this observer cannot eat core negotiation. Existing raw
|
||||
hooks may still eat CAP before this hook according to their normal priorities.
|
||||
|
||||
C: `zoitechat_register_capability(ph, name, flags, connection_id, userdata)`;
|
||||
flags=0, ID=-1 for any. Check get_info("api_plugin_caps") for "1" before using
|
||||
the appended Windows slot. Use existing server CAP hooks for notifications.
|
||||
Requests are split at complete token boundaries within the existing wire limit.
|
||||
|
||||
```python
|
||||
registration = zoitechat.register_capability('draft/example')
|
||||
zoitechat.hook_capability('draft/example', lambda event, data:
|
||||
event.context.prnt('{}: {}'.format(event.subcommand, event.enabled)))
|
||||
```
|
||||
@@ -0,0 +1,24 @@
|
||||
# Optional plugin API additions (discussion #144)
|
||||
|
||||
Existing hooks, callbacks, return values, context lookup, strings and menu behaviour
|
||||
remain unchanged. The C plugin function table gains four fixed append-only slots;
|
||||
unused slots are NULL. Older binaries use the unchanged prefix. A plugin must check
|
||||
the documented get_info() feature key before accessing a new slot on an older host.
|
||||
New Python modules are installed by Meson and the existing Windows copy/installer
|
||||
pipeline. Only modules present in the installation are exported; imports of
|
||||
zoitechat, hexchat and xchat retain their compatibility aliases.
|
||||
|
||||
All APIs run on the client main thread unless explicitly documented otherwise.
|
||||
New reserved flags must be zero. These changes do not make old API calls thread-safe.
|
||||
|
||||
Nested hook dispatch defers deleted-hook reclamation until its outermost return,
|
||||
so new observers can safely emit events and unhook themselves or other hooks.
|
||||
|
||||
The optional asyncio module requires Python 3.7 or newer; older supported
|
||||
interpreters skip that module and retain the existing scripting API.
|
||||
|
||||
Python reload drops the script/API modules while retaining the embedding module
|
||||
so callback and lib.ph pointer types keep the same CFFI identity.
|
||||
|
||||
Server-hook insertion matches both raw and attrs hooks so documented priorities
|
||||
are honoured for mixed registrations, including new filtered attrs callbacks.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Connection and context navigation
|
||||
|
||||
`get_connections()` returns one Connection snapshot per live connection:
|
||||
`id`, `network`, `server`, `context`. Connection IDs distinguish simultaneous
|
||||
connections with identical names. Prefer its server-tab context where present.
|
||||
`get_connection(context=None)` finds the owner of a live context.
|
||||
|
||||
`get_contexts(connection_id=None, types=None)` lists live contexts, optionally
|
||||
filtered by connection ID (or a Connection object) and session types: server=1,
|
||||
channel=2, query=3. `connection.contexts(types=None)` is a convenience method.
|
||||
`find_context_by_id(connection_id, channel=None)` / `connection.find(channel)`
|
||||
resolve using that connection's IRC comparator; no channel selects the server tab.
|
||||
Missing/closed contexts return None. Existing find_context() remains unchanged.
|
||||
|
||||
These helpers use the existing channels list's ID/context fields, so no new C
|
||||
context ABI is needed. Snapshots are not persistent IDs across client restarts;
|
||||
refresh them after closing/recreating connections. Always check Context.set()
|
||||
when explicitly selecting a context that might have closed.
|
||||
|
||||
```python
|
||||
connection = zoitechat.get_connection()
|
||||
for context in connection.contexts(types=(2,)):
|
||||
context.prnt('A channel on this exact connection')
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
# Semantic IRC hooks
|
||||
|
||||
`on_join`, `on_part`, `on_quit`, `on_nick`, `on_kick`, `on_message`, and
|
||||
`on_notice` accept `(callback, userdata=None, priority=PRI_NORM)`. The general
|
||||
form is `hook_event(command, callback, userdata=None, priority=PRI_NORM)`.
|
||||
Callbacks receive `(event, userdata)`, where the immutable IRCEvent has
|
||||
command, prefix, nick, user, host, params, context, connection_id and time.
|
||||
`params` preserves the trailing parameter, including spaces; JOIN parameters
|
||||
also retain extended-join account/realname data when supplied by the server.
|
||||
|
||||
These wrappers use existing server hooks, before core command processing;
|
||||
return the ordinary EAT_* constants. EAT_ZOITECHAT suppresses the entire IRC
|
||||
command (including client state updates), not just its text. Use EAT_NONE for
|
||||
observation. Other plugins' priority and eating rules remain unchanged.
|
||||
The context is the core-selected context, which can be the server tab when a
|
||||
query/channel does not yet exist. QUIT is one event per incoming command,
|
||||
not one callback per affected channel. `event.time` is the existing server-time
|
||||
attribute (zero when unavailable); other message tags are not exposed here.
|
||||
|
||||
```python
|
||||
def joined(event, userdata):
|
||||
event.context.prnt('{} joined {}'.format(event.nick, event.params[0]))
|
||||
return zoitechat.EAT_NONE
|
||||
|
||||
zoitechat.on_join(joined)
|
||||
```
|
||||
@@ -0,0 +1,19 @@
|
||||
# Python descriptor hooks
|
||||
|
||||
`hook_fd(fd, callback, userdata=None, flags=FD_READ)` returns an ordinary hook
|
||||
handle accepted by `unhook()`. Callback arguments are `(fd, ready_flags, userdata)`;
|
||||
return True to retain the watch and False/None to remove it. Exceptions print a
|
||||
traceback and remove the watch. Unloading the script removes its watches.
|
||||
The callback runs in the context captured during registration, and that context
|
||||
is restored afterwards. A closed context removes the watch.
|
||||
|
||||
Use nonblocking IO and bounded reads/writes. The descriptor belongs to the script;
|
||||
unhook before closing it. On Windows socket descriptors use the default flags;
|
||||
CRT descriptors need FD_NOTSOCKET. The existing C API uses int descriptors: values
|
||||
outside its range are rejected rather than truncated. This does not add threads.
|
||||
|
||||
Example:
|
||||
```python
|
||||
watch = zoitechat.hook_fd(sock.fileno(), on_readable, flags=zoitechat.FD_READ)
|
||||
# def on_readable(fd, flags, userdata): ...; return True
|
||||
```
|
||||
@@ -0,0 +1,24 @@
|
||||
# Core-filtered hooks
|
||||
|
||||
Python: `hook_server_filtered(name, callback, userdata=None, priority=PRI_NORM,
|
||||
connection_id=None, channel=None)` and `hook_print_filtered(...)`.
|
||||
Callbacks match the existing attrs variants: `(word, word_eol, userdata, attrs)`.
|
||||
The connection ID distinguishes duplicate names; None/-1 means any connection
|
||||
(including connection ID zero). A channel restriction uses that connection's
|
||||
IRC comparator. None means any context; an empty string is an exact restriction.
|
||||
|
||||
C: the corresponding zoitechat_hook_*_filtered functions accept
|
||||
`(ph, name, pri, flags, connection_id, channel, callback, userdata)`;
|
||||
flags=0, ID=-1 for any and NULL channel for any. Check
|
||||
`get_info("api_context_filters")` for "1" before accessing the new Windows slots.
|
||||
|
||||
Filtering takes place before Python/interpreter entry. Existing unrestricted
|
||||
hooks, priority, EAT semantics and unload/unhook handling are unchanged.
|
||||
Filters refer to the core-selected context, not an IRC target parsed from the
|
||||
line: unknown queries/channels can still have a server-tab context, and QUIT
|
||||
has its usual single server-hook context. Connection IDs last only this process.
|
||||
|
||||
```python
|
||||
zoitechat.hook_print_filtered('Channel Message', callback,
|
||||
connection_id=zoitechat.get_prefs('id'), channel='#zoitechat')
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
# Explicit IRC comparison keys
|
||||
|
||||
`IRCName(text, context=None, casemapping=None, connection_id=None)` is an
|
||||
immutable helper, not a str subclass. Construct it explicitly for dictionary/set
|
||||
keys; existing get_info/list/event values remain normal strings.
|
||||
Equality and hashing include the connection ID and captured casemapping, so
|
||||
identical-looking names on different connections are different keys. An IRCName
|
||||
never compares equal to a plain str. `str(key)` / `key.text` recover the spelling.
|
||||
|
||||
`irc_casefold(text, casemapping='rfc1459')` supports ascii, strict-rfc1459 and
|
||||
rfc1459. Only ASCII IRC characters fold; Unicode casefold would be incorrect.
|
||||
The new `get_info('casemapping')` reports the comparator actually used by core.
|
||||
Current master handles ASCII and RFC1459; this patch does not change its handling
|
||||
of advertised strict-rfc1459. An explicit mapping is available for scripts that
|
||||
need strict-rfc1459 independently. Keys capture their mapping so hashes stay
|
||||
stable; rebuild a collection if the connection's mapping changes.
|
||||
|
||||
```python
|
||||
seen = {zoitechat.IRCName('[SomeNick]')}
|
||||
assert zoitechat.IRCName('{somenick}') in seen # RFC1459 connection
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
# Post-display print observers
|
||||
|
||||
Python: `hook_print_after(name, callback, userdata=None, priority=PRI_NORM)`.
|
||||
The callback is `(word, word_eol, userdata, attrs)`, matching hook_print_attrs().
|
||||
It runs after display_event() has handled the text, so prnt() follows the original
|
||||
text. Return values are ignored. Events eaten by a pre-hook or hidden by client
|
||||
preferences do not invoke these observers. This is post-display notification,
|
||||
not a notification that the entire incoming IRC command has finished updating state.
|
||||
|
||||
C: `zoitechat_hook_print_after(ph, name, priority, flags, callback, userdata)`;
|
||||
flags must be zero. Check `get_info("api_hook_print_after")` for "1" before
|
||||
accessing the appended Windows function-table slot. Existing pre-hook timing is
|
||||
unchanged. Unhook/unload use existing lifecycle functions. Recursive emission
|
||||
skips an already-running observer to prevent it immediately invoking itself.
|
||||
|
||||
```python
|
||||
def after(word, word_eol, userdata, attrs):
|
||||
zoitechat.prnt('This follows the original channel message')
|
||||
|
||||
zoitechat.hook_print_after('Channel Message', after)
|
||||
```
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
project('zoitechat', 'c',
|
||||
version: run_command('cat', 'VERSION', check: true).stdout().strip(),
|
||||
meson_version: '>= 0.55.0',
|
||||
meson_version: '>= 0.59.0',
|
||||
default_options: [
|
||||
'c_std=c17',
|
||||
'buildtype=debugoptimized',
|
||||
|
||||
@@ -710,6 +710,7 @@ static int handle_fishlim(char *word[], char *word_eol[], void *userdata) {
|
||||
gtk_box_pack_start(GTK_BOX(content), grid, TRUE, TRUE, 0);
|
||||
|
||||
fishlim_target_entry = gtk_entry_new();
|
||||
gtk_widget_set_hexpand(fishlim_target_entry, TRUE);
|
||||
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);
|
||||
@@ -760,6 +761,7 @@ static int handle_fishlim(char *word[], char *word_eol[], void *userdata) {
|
||||
g_signal_connect(view, "row-activated", G_CALLBACK(fishlim_gui_row_activated), NULL);
|
||||
|
||||
scroll = gtk_scrolled_window_new(NULL, NULL);
|
||||
gtk_widget_set_hexpand(scroll, TRUE);
|
||||
gtk_widget_set_vexpand(scroll, TRUE);
|
||||
gtk_container_add(GTK_CONTAINER(scroll), view);
|
||||
gtk_grid_attach(GTK_GRID(grid), scroll, 0, 6, 3, 1);
|
||||
|
||||
@@ -383,3 +383,24 @@ def find_context(server=None, channel=None):
|
||||
return None
|
||||
|
||||
return Context(ctx)
|
||||
|
||||
|
||||
# Optional additive APIs live in separate modules so builds can select features.
|
||||
def __load_additive_features():
|
||||
import importlib
|
||||
import os
|
||||
|
||||
for suffix in ('fd', 'events', 'async', 'names', 'contexts',
|
||||
'post', 'filters', 'caps'):
|
||||
if suffix == 'async' and sys.version_info < (3, 7):
|
||||
continue
|
||||
name = '_zoitechat_' + suffix
|
||||
if not os.path.isfile(os.path.join(os.path.dirname(__file__), name + '.py')):
|
||||
continue
|
||||
module = importlib.import_module(name)
|
||||
for symbol in module.__all__:
|
||||
globals()[symbol] = getattr(module, symbol)
|
||||
__all__.append(symbol)
|
||||
|
||||
|
||||
__load_additive_features()
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
"""Opt-in asyncio tasks pumped without blocking the client main loop."""
|
||||
import asyncio
|
||||
import operator
|
||||
import traceback
|
||||
import _zoitechat as api
|
||||
|
||||
__all__ = ['AsyncTask', 'run_async']
|
||||
|
||||
|
||||
class AsyncTask:
|
||||
"""A script-owned task and isolated event loop; cancel() is cooperative."""
|
||||
def __init__(self, coroutine, plugin, context, interval):
|
||||
self._plugin = plugin
|
||||
self._context = context
|
||||
self._loop = asyncio.new_event_loop()
|
||||
self._task = self._loop.create_task(coroutine)
|
||||
self._closed = False
|
||||
self._unload_hook = api.hook_unload(lambda data: self._close())
|
||||
self._timer_hook = api.hook_timer(interval, self._tick)
|
||||
|
||||
def done(self):
|
||||
return self._task.done()
|
||||
|
||||
def cancel(self):
|
||||
if not self._closed:
|
||||
return self._task.cancel()
|
||||
return False
|
||||
|
||||
def result(self):
|
||||
return self._task.result()
|
||||
|
||||
def _pump(self):
|
||||
# A ready stop callback forces the selector's timeout to zero.
|
||||
self._loop.call_soon(self._loop.stop)
|
||||
self._loop.run_forever()
|
||||
|
||||
def _close(self):
|
||||
if self._closed:
|
||||
return
|
||||
self._closed = True
|
||||
previous = api.get_context()
|
||||
self._context.set()
|
||||
try:
|
||||
pending = asyncio.all_tasks(self._loop)
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
# Bound cleanup work; tasks must cooperate with cancellation.
|
||||
for unused in range(3):
|
||||
if not any(not task.done() for task in pending):
|
||||
break
|
||||
self._pump()
|
||||
for task in pending:
|
||||
if not task.done():
|
||||
self._loop.call_exception_handler({
|
||||
'message': 'Script task ignored cancellation during unload',
|
||||
'task': task,
|
||||
})
|
||||
finally:
|
||||
self._loop.close()
|
||||
previous.set()
|
||||
|
||||
def _tick(self, userdata):
|
||||
if self._closed:
|
||||
return False
|
||||
previous = api.get_context()
|
||||
if not self._context.set():
|
||||
self._close()
|
||||
self._plugin.remove_hook(self._unload_hook)
|
||||
return False
|
||||
try:
|
||||
self._pump()
|
||||
if not self._task.done():
|
||||
return True
|
||||
try:
|
||||
self._task.result()
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
self._close()
|
||||
self._plugin.remove_hook(self._unload_hook)
|
||||
return False
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
self._close()
|
||||
self._plugin.remove_hook(self._unload_hook)
|
||||
return False
|
||||
finally:
|
||||
previous.set()
|
||||
|
||||
|
||||
def run_async(coroutine, context=None, interval=10):
|
||||
"""Schedule a coroutine in a captured context; return an AsyncTask handle."""
|
||||
interval = operator.index(interval)
|
||||
if interval < 1 or interval > 2147483647:
|
||||
raise ValueError('interval must be a positive C int in milliseconds')
|
||||
if not asyncio.iscoroutine(coroutine):
|
||||
raise TypeError('run_async expects a coroutine object')
|
||||
plugin = api.__get_current_plugin()
|
||||
return AsyncTask(coroutine, plugin, context or api.get_context(), interval)
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Script-owned capability requests and structured negotiation observations."""
|
||||
from collections import namedtuple
|
||||
import operator
|
||||
import _zoitechat as api
|
||||
from _zoitechat_embedded import ffi, lib
|
||||
|
||||
__all__ = ['CapabilityEvent', 'register_capability', 'hook_capability']
|
||||
CapabilityEvent = namedtuple('CapabilityEvent',
|
||||
'name subcommand value enabled context connection_id')
|
||||
|
||||
|
||||
def register_capability(name, userdata=None, connection_id=None):
|
||||
"""Request an advertised capability on next negotiation; unhook releases ownership."""
|
||||
connection_id = -1 if connection_id is None else operator.index(connection_id)
|
||||
if connection_id < -1 or connection_id > 2147483647:
|
||||
raise ValueError('connection_id must fit the C API; -1 means any')
|
||||
plugin = api.__get_current_plugin()
|
||||
hook = plugin.add_hook(None, userdata)
|
||||
handle = lib.zoitechat_register_capability(lib.ph, name.encode(), 0,
|
||||
connection_id, hook.handle)
|
||||
if handle == ffi.NULL:
|
||||
hook.is_unload = True
|
||||
plugin.remove_hook(id(hook))
|
||||
raise ValueError('invalid capability name, reserved capability or connection ID')
|
||||
hook.zoitechat_hook = handle
|
||||
return id(hook)
|
||||
|
||||
|
||||
def hook_capability(name, callback, userdata=None, priority=api.PRI_NORM):
|
||||
"""Observe LS/NEW/ACK/NAK/DEL/LIST; this hook does not itself request the capability."""
|
||||
if not callable(callback):
|
||||
raise TypeError('callback must be callable')
|
||||
|
||||
def dispatch(word, word_eol, data):
|
||||
if not word_eol:
|
||||
return api.EAT_NONE
|
||||
line = word_eol[0]
|
||||
if line.startswith(':'):
|
||||
unused, separator, line = line.partition(' ')
|
||||
parts = line.split(' ', 3)
|
||||
if len(parts) != 4 or parts[0].upper() != 'CAP':
|
||||
return api.EAT_NONE
|
||||
subcommand, tokens = parts[2].upper(), parts[3]
|
||||
if subcommand not in ('LS', 'NEW', 'ACK', 'NAK', 'DEL', 'LIST'):
|
||||
return api.EAT_NONE
|
||||
if tokens.startswith('* '):
|
||||
tokens = tokens[2:]
|
||||
tokens = tokens.lstrip(':')
|
||||
for token in tokens.split():
|
||||
negative = token.startswith('-')
|
||||
cap, equals, value = token.lstrip('-').partition('=')
|
||||
if cap != name:
|
||||
continue
|
||||
enabled = None
|
||||
if subcommand in ('ACK', 'LIST'):
|
||||
enabled = not negative
|
||||
elif subcommand in ('DEL', 'NAK'):
|
||||
enabled = False
|
||||
event = CapabilityEvent(cap, subcommand, value if equals else None,
|
||||
enabled, api.get_context(), api.get_prefs('id'))
|
||||
callback(event, data)
|
||||
return api.EAT_NONE # observing CAP must not suppress core negotiation
|
||||
|
||||
return api.hook_server('CAP', dispatch, userdata, priority)
|
||||
@@ -0,0 +1,75 @@
|
||||
"""Navigate live connections by ID instead of ambiguous server/network names."""
|
||||
from collections import namedtuple
|
||||
import operator
|
||||
import _zoitechat as api
|
||||
|
||||
__all__ = ['Connection', 'get_connections', 'get_connection', 'get_contexts',
|
||||
'find_context_by_id']
|
||||
|
||||
|
||||
class Connection(namedtuple('_Connection', 'id network server context')):
|
||||
__slots__ = ()
|
||||
|
||||
def contexts(self, types=None):
|
||||
return get_contexts(self.id, types)
|
||||
|
||||
def find(self, channel=None):
|
||||
return find_context_by_id(self.id, channel)
|
||||
|
||||
|
||||
def _id(connection):
|
||||
return operator.index(connection.id if isinstance(connection, Connection) else connection)
|
||||
|
||||
|
||||
def get_connections():
|
||||
"""Return one snapshot per live connection, including disconnected ones."""
|
||||
entries = api.get_list('channels')
|
||||
by_id = {}
|
||||
order = []
|
||||
for entry in entries:
|
||||
if entry.id not in by_id:
|
||||
order.append(entry.id)
|
||||
if entry.id not in by_id or entry.type == 1:
|
||||
by_id[entry.id] = Connection(entry.id, entry.network, entry.server,
|
||||
entry.context)
|
||||
return [by_id[connection_id] for connection_id in order]
|
||||
|
||||
|
||||
def get_connection(context=None):
|
||||
"""Find the owning connection of a Context; return None when it has closed."""
|
||||
context = context or api.get_context()
|
||||
for entry in api.get_list('channels'):
|
||||
if entry.context == context:
|
||||
for connection in get_connections():
|
||||
if connection.id == entry.id:
|
||||
return connection
|
||||
return None
|
||||
|
||||
|
||||
def get_contexts(connection_id=None, types=None):
|
||||
"""Return contexts, optionally restricted to a connection and session types."""
|
||||
if connection_id is not None:
|
||||
connection_id = _id(connection_id)
|
||||
if types is not None:
|
||||
types = frozenset(types)
|
||||
return [entry.context for entry in api.get_list('channels')
|
||||
if (connection_id is None or entry.id == connection_id)
|
||||
and (types is None or entry.type in types)]
|
||||
|
||||
|
||||
def find_context_by_id(connection_id, channel=None):
|
||||
"""Resolve a live context on exactly one connection; None selects its server tab."""
|
||||
connection_id = _id(connection_id)
|
||||
previous = api.get_context()
|
||||
try:
|
||||
for entry in api.get_list('channels'):
|
||||
if entry.id != connection_id:
|
||||
continue
|
||||
if channel is None:
|
||||
if entry.type == 1:
|
||||
return entry.context
|
||||
elif entry.context.set() and api.nickcmp(entry.channel, channel) == 0:
|
||||
return entry.context
|
||||
finally:
|
||||
previous.set()
|
||||
return None
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Structured, opt-in hooks before the IRC command's core processing."""
|
||||
from collections import namedtuple
|
||||
import _zoitechat as api
|
||||
|
||||
__all__ = ['IRCEvent', 'hook_event', 'on_join', 'on_part', 'on_quit',
|
||||
'on_nick', 'on_kick', 'on_message', 'on_notice']
|
||||
IRCEvent = namedtuple('IRCEvent', 'command prefix nick user host params context connection_id time')
|
||||
_SUPPORTED = frozenset(('JOIN', 'PART', 'QUIT', 'NICK', 'KICK', 'PRIVMSG', 'NOTICE'))
|
||||
|
||||
|
||||
def _parse(line):
|
||||
prefix = ''
|
||||
if line.startswith(':'):
|
||||
prefix, separator, line = line[1:].partition(' ')
|
||||
if not separator:
|
||||
return None
|
||||
command, separator, rest = line.partition(' ')
|
||||
params = []
|
||||
while rest:
|
||||
rest = rest.lstrip(' ')
|
||||
if rest.startswith(':'):
|
||||
params.append(rest[1:])
|
||||
break
|
||||
param, separator, rest = rest.partition(' ')
|
||||
if param:
|
||||
params.append(param)
|
||||
nick, marker, userhost = prefix.partition('!')
|
||||
user, marker, host = userhost.partition('@')
|
||||
return command.upper(), prefix, nick, user, host, tuple(params)
|
||||
|
||||
|
||||
def hook_event(command, callback, userdata=None, priority=api.PRI_NORM):
|
||||
"""callback(event, userdata) returns existing EAT_* constants."""
|
||||
command = command.upper()
|
||||
if command not in _SUPPORTED:
|
||||
raise ValueError('unsupported semantic event: ' + command)
|
||||
if not callable(callback):
|
||||
raise TypeError('callback must be callable')
|
||||
|
||||
def dispatch(word, word_eol, data, attrs):
|
||||
if not word_eol:
|
||||
return api.EAT_NONE
|
||||
parsed = _parse(word_eol[0])
|
||||
if parsed is None or parsed[0] != command:
|
||||
return api.EAT_NONE
|
||||
event = IRCEvent(*(parsed + (api.get_context(), api.get_prefs('id'), attrs.time)))
|
||||
return callback(event, data)
|
||||
|
||||
return api.hook_server_attrs(command, dispatch, userdata, priority)
|
||||
|
||||
|
||||
def on_join(callback, userdata=None, priority=api.PRI_NORM):
|
||||
return hook_event('JOIN', callback, userdata, priority)
|
||||
|
||||
|
||||
def on_part(callback, userdata=None, priority=api.PRI_NORM):
|
||||
return hook_event('PART', callback, userdata, priority)
|
||||
|
||||
|
||||
def on_quit(callback, userdata=None, priority=api.PRI_NORM):
|
||||
return hook_event('QUIT', callback, userdata, priority)
|
||||
|
||||
|
||||
def on_nick(callback, userdata=None, priority=api.PRI_NORM):
|
||||
return hook_event('NICK', callback, userdata, priority)
|
||||
|
||||
|
||||
def on_kick(callback, userdata=None, priority=api.PRI_NORM):
|
||||
return hook_event('KICK', callback, userdata, priority)
|
||||
|
||||
|
||||
def on_message(callback, userdata=None, priority=api.PRI_NORM):
|
||||
return hook_event('PRIVMSG', callback, userdata, priority)
|
||||
|
||||
|
||||
def on_notice(callback, userdata=None, priority=api.PRI_NORM):
|
||||
return hook_event('NOTICE', callback, userdata, priority)
|
||||
@@ -0,0 +1,39 @@
|
||||
"""File descriptor readiness hooks; callbacks execute on the main thread."""
|
||||
import operator
|
||||
import _zoitechat as api
|
||||
from _zoitechat_embedded import ffi, lib
|
||||
|
||||
__all__ = ['FD_READ', 'FD_WRITE', 'FD_EXCEPTION', 'FD_NOTSOCKET', 'hook_fd']
|
||||
FD_READ, FD_WRITE, FD_EXCEPTION, FD_NOTSOCKET = 1, 2, 4, 8
|
||||
|
||||
|
||||
def hook_fd(fd, callback, userdata=None, flags=FD_READ):
|
||||
"""callback(fd, ready_flags, userdata): True keeps the watch, False removes it."""
|
||||
fd = operator.index(fd)
|
||||
flags = operator.index(flags)
|
||||
if fd < 0 or fd > 2147483647:
|
||||
raise ValueError('fd must fit the C API nonnegative int descriptor')
|
||||
if flags & ~15 or not flags & 7:
|
||||
raise ValueError('flags must select READ, WRITE or EXCEPTION')
|
||||
if not callable(callback):
|
||||
raise TypeError('callback must be callable')
|
||||
plugin = api.__get_current_plugin()
|
||||
context = api.get_context()
|
||||
|
||||
def dispatch(descriptor, ready, data):
|
||||
previous = api.get_context()
|
||||
if not context.set():
|
||||
return False
|
||||
try:
|
||||
return callback(descriptor, ready, data)
|
||||
finally:
|
||||
previous.set()
|
||||
|
||||
hook = plugin.add_hook(dispatch, userdata)
|
||||
handle = lib.zoitechat_hook_fd(lib.ph, fd, flags, lib._on_fd_hook, hook.handle)
|
||||
if handle == ffi.NULL:
|
||||
hook.is_unload = True
|
||||
plugin.remove_hook(id(hook))
|
||||
raise RuntimeError('unable to watch descriptor')
|
||||
hook.zoitechat_hook = handle
|
||||
return id(hook)
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Context restrictions evaluated by core before entering Python."""
|
||||
import operator
|
||||
import _zoitechat as api
|
||||
from _zoitechat_embedded import ffi, lib
|
||||
|
||||
__all__ = ['hook_server_filtered', 'hook_print_filtered']
|
||||
|
||||
|
||||
def _register(name, callback, userdata, priority, connection_id, channel, server):
|
||||
if not callable(callback):
|
||||
raise TypeError('callback must be callable')
|
||||
connection_id = -1 if connection_id is None else operator.index(connection_id)
|
||||
if connection_id < -1 or connection_id > 2147483647:
|
||||
raise ValueError('connection_id must fit the C API; -1 means any')
|
||||
plugin = api.__get_current_plugin()
|
||||
hook = plugin.add_hook(callback, userdata)
|
||||
register = lib.zoitechat_hook_server_filtered if server else lib.zoitechat_hook_print_filtered
|
||||
dispatch = lib._on_server_attrs_hook if server else lib._on_print_attrs_hook
|
||||
handle = register(lib.ph, name.encode(), priority, 0, connection_id,
|
||||
ffi.NULL if channel is None else channel.encode(), dispatch, hook.handle)
|
||||
if handle == ffi.NULL:
|
||||
hook.is_unload = True
|
||||
plugin.remove_hook(id(hook))
|
||||
raise RuntimeError('unable to install filtered hook')
|
||||
hook.zoitechat_hook = handle
|
||||
return id(hook)
|
||||
|
||||
|
||||
def hook_server_filtered(name, callback, userdata=None, priority=api.PRI_NORM,
|
||||
connection_id=None, channel=None):
|
||||
return _register(name, callback, userdata, priority, connection_id, channel, True)
|
||||
|
||||
|
||||
def hook_print_filtered(name, callback, userdata=None, priority=api.PRI_NORM,
|
||||
connection_id=None, channel=None):
|
||||
return _register(name, callback, userdata, priority, connection_id, channel, False)
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Explicit immutable IRC names; existing string-returning APIs are unchanged."""
|
||||
import _zoitechat as api
|
||||
|
||||
__all__ = ['IRCName', 'irc_casefold']
|
||||
|
||||
|
||||
def irc_casefold(text, casemapping='rfc1459'):
|
||||
"""Fold ASCII IRC characters only; never apply Unicode lower()/casefold()."""
|
||||
if casemapping not in ('ascii', 'strict-rfc1459', 'rfc1459'):
|
||||
raise ValueError('unsupported IRC casemapping')
|
||||
extra = {'[': '{', ']': '}', '\\': '|'} if casemapping != 'ascii' else {}
|
||||
if casemapping == 'rfc1459':
|
||||
extra['^'] = '~'
|
||||
return ''.join(chr(ord(c) + 32) if 'A' <= c <= 'Z' else extra.get(c, c)
|
||||
for c in text)
|
||||
|
||||
|
||||
class IRCName:
|
||||
"""Equality/hash include connection identity and the captured casemapping."""
|
||||
__slots__ = ('_text', '_casemapping', '_connection_id', '_key')
|
||||
|
||||
def __init__(self, text, context=None, casemapping=None, connection_id=None):
|
||||
context = context or api.get_context()
|
||||
if casemapping is None:
|
||||
casemapping = context.get_info('casemapping')
|
||||
if casemapping is None:
|
||||
raise RuntimeError('host does not expose its comparison casemapping')
|
||||
if connection_id is None:
|
||||
previous = api.get_context()
|
||||
if not context.set():
|
||||
raise ValueError('context has closed')
|
||||
try:
|
||||
connection_id = api.get_prefs('id')
|
||||
finally:
|
||||
previous.set()
|
||||
object.__setattr__(self, '_text', text)
|
||||
object.__setattr__(self, '_casemapping', casemapping)
|
||||
object.__setattr__(self, '_connection_id', connection_id)
|
||||
object.__setattr__(self, '_key', (connection_id, casemapping,
|
||||
irc_casefold(text, casemapping)))
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
raise AttributeError('IRCName is immutable')
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
return self._text
|
||||
|
||||
@property
|
||||
def casemapping(self):
|
||||
return self._casemapping
|
||||
|
||||
@property
|
||||
def connection_id(self):
|
||||
return self._connection_id
|
||||
|
||||
def __str__(self):
|
||||
return self._text
|
||||
|
||||
def __repr__(self):
|
||||
return 'IRCName({!r}, casemapping={!r}, connection_id={!r})'.format(
|
||||
self._text, self._casemapping, self._connection_id)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, IRCName):
|
||||
return NotImplemented
|
||||
return self._key == other._key
|
||||
|
||||
def __ne__(self, other):
|
||||
result = self.__eq__(other)
|
||||
return NotImplemented if result is NotImplemented else not result
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self._key)
|
||||
@@ -0,0 +1,21 @@
|
||||
"""Opt-in post-display print observers."""
|
||||
import _zoitechat as api
|
||||
from _zoitechat_embedded import ffi, lib
|
||||
|
||||
__all__ = ['hook_print_after']
|
||||
|
||||
|
||||
def hook_print_after(name, callback, userdata=None, priority=api.PRI_NORM):
|
||||
"""callback(word, word_eol, userdata, attrs); its return value is ignored."""
|
||||
if not callable(callback):
|
||||
raise TypeError('callback must be callable')
|
||||
plugin = api.__get_current_plugin()
|
||||
hook = plugin.add_hook(callback, userdata)
|
||||
handle = lib.zoitechat_hook_print_after(lib.ph, name.encode(), priority, 0,
|
||||
lib._on_print_attrs_hook, hook.handle)
|
||||
if handle == ffi.NULL:
|
||||
hook.is_unload = True
|
||||
plugin.remove_hook(id(hook))
|
||||
raise RuntimeError('unable to install post-display hook')
|
||||
hook.zoitechat_hook = handle
|
||||
return id(hook)
|
||||
@@ -38,6 +38,7 @@ extern "Python" int _on_print_attrs_hook(char **, zoitechat_event_attrs *, void
|
||||
extern "Python" int _on_server_hook(char **, char **, void *);
|
||||
extern "Python" int _on_server_attrs_hook(char **, char **, zoitechat_event_attrs *, void *);
|
||||
extern "Python" int _on_timer_hook(void *);
|
||||
extern "Python" int _on_fd_hook(int, int, void *);
|
||||
|
||||
extern "Python" int _on_plugin_init(char **, char **, char **, char *, char *);
|
||||
extern "Python" int _on_plugin_deinit(void);
|
||||
|
||||
@@ -27,7 +27,11 @@ python3_source = custom_target('python-bindings',
|
||||
command: [find_program('generate_plugin.py'), '@INPUT@', '@OUTPUT@']
|
||||
)
|
||||
|
||||
install_data(['_zoitechat.py', 'zoitechat.py', 'xchat.py', 'hexchat.py'],
|
||||
# Include any selected additive API modules alongside the compatibility modules.
|
||||
python_api_files = run_command(python3, '-c',
|
||||
'import glob,sys; print("\\n".join(sorted(glob.glob(sys.argv[1] + "/_zoitechat*.py"))))',
|
||||
meson.current_source_dir(), check: true).stdout().strip().split('\n')
|
||||
install_data(python_api_files + ['zoitechat.py', 'xchat.py', 'hexchat.py'],
|
||||
install_dir: join_paths(get_option('libdir'), 'zoitechat/python')
|
||||
)
|
||||
|
||||
|
||||
@@ -304,6 +304,28 @@ def _on_timer_hook(userdata):
|
||||
return 0
|
||||
|
||||
|
||||
@ffi.def_extern()
|
||||
def _on_fd_hook(fd, flags, userdata):
|
||||
hook = ffi.from_handle(userdata)
|
||||
keep = False
|
||||
try:
|
||||
keep = bool(hook.callback(fd, flags, hook.userdata))
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
if not keep:
|
||||
# The C watch removes itself on return; do not unhook it a second time.
|
||||
try:
|
||||
hook.is_unload = True
|
||||
# from_handle() yields a weak proxy: id(proxy) is not id(Hook).
|
||||
for existing in hook.plugin.hooks:
|
||||
if existing == hook:
|
||||
hook.plugin.remove_hook(id(existing))
|
||||
break
|
||||
except ReferenceError:
|
||||
pass # the callback already unhooked itself
|
||||
return int(keep)
|
||||
|
||||
|
||||
@ffi.def_extern()
|
||||
def _on_say_command(word, word_eol, userdata):
|
||||
"""Handle input in the special >>python<< tab.
|
||||
@@ -590,7 +612,12 @@ def _on_plugin_deinit():
|
||||
sys.stderr = sys.__stderr__
|
||||
pydoc.help = pydoc.Helper()
|
||||
|
||||
for mod in ('_zoitechat', 'zoitechat', 'xchat', '_zoitechat_embedded'):
|
||||
# CFFI callbacks keep the embedding module's ffi/lib identity for the
|
||||
# process lifetime; deleting it creates incompatible types on reload.
|
||||
for mod in ('_zoitechat', 'zoitechat', 'hexchat', 'xchat',
|
||||
'_zoitechat_fd', '_zoitechat_events', '_zoitechat_async',
|
||||
'_zoitechat_names', '_zoitechat_contexts', '_zoitechat_post',
|
||||
'_zoitechat_filters', '_zoitechat_caps'):
|
||||
try:
|
||||
del sys.modules[mod]
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
<None Include="xchat.py" />
|
||||
<None Include="hexchat.py" />
|
||||
<None Include="_zoitechat.py" />
|
||||
<None Include="_zoitechat_*.py" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="$(IntDir)python.c" />
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
<None Include="xchat.py" />
|
||||
<None Include="hexchat.py" />
|
||||
<None Include="_zoitechat.py" />
|
||||
<None Include="_zoitechat_*.py" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="$(IntDir)python.c" />
|
||||
|
||||
@@ -26,7 +26,7 @@ if system == 'linux' or system == 'gnu' or system.startswith('gnu/') or system =
|
||||
sysinfo_cargs += '-DHAVE_LIBPCI'
|
||||
sysinfo_sources += 'unix/pci.c'
|
||||
|
||||
picidsdir = libpci.get_pkgconfig_variable('idsdir')
|
||||
picidsdir = libpci.get_variable(pkgconfig : 'idsdir')
|
||||
pciids = join_paths(picidsdir, 'pci.ids')
|
||||
sysinfo_cargs += '-DPCIIDS_FILE="@0@"'.format(pciids)
|
||||
endif
|
||||
|
||||
@@ -30,6 +30,7 @@ src/fe-gtk/fe-gtk.c
|
||||
src/fe-gtk/fkeys.c
|
||||
src/fe-gtk/gtkutil.c
|
||||
src/fe-gtk/ignoregui.c
|
||||
src/fe-gtk/inline-image.c
|
||||
src/fe-gtk/joind.c
|
||||
src/fe-gtk/maingui.c
|
||||
src/fe-gtk/menu.c
|
||||
|
||||
@@ -443,6 +443,7 @@ const struct prefs vars[] =
|
||||
{"gui_tab_chans", P_OFFINT (hex_gui_tab_chans), TYPE_BOOL},
|
||||
{"gui_tab_dialogs", P_OFFINT (hex_gui_tab_dialogs), TYPE_BOOL},
|
||||
{"gui_tab_dots", P_OFFINT (hex_gui_tab_dots), TYPE_BOOL},
|
||||
{"gui_tab_font", P_OFFSET (hex_gui_tab_font), TYPE_STR},
|
||||
{"gui_tab_icons", P_OFFINT (hex_gui_tab_icons), TYPE_BOOL},
|
||||
{"gui_dark_mode", P_OFFINT (hex_gui_dark_mode), TYPE_INT},
|
||||
{"gui_gtk3_variant", P_OFFINT (hex_gui_gtk3_variant), TYPE_INT},
|
||||
@@ -565,6 +566,7 @@ const struct prefs vars[] =
|
||||
{"net_proxy_use", P_OFFINT (hex_net_proxy_use), TYPE_INT},
|
||||
{"net_proxy_user", P_OFFSET (hex_net_proxy_user), TYPE_STR},
|
||||
{"net_reconnect_delay", P_OFFINT (hex_net_reconnect_delay), TYPE_INT},
|
||||
{"net_remote_media", P_OFFINT (hex_net_remote_media), TYPE_BOOL},
|
||||
{"net_throttle", P_OFFINT (hex_net_throttle), TYPE_BOOL},
|
||||
|
||||
{"notify_timeout", P_OFFINT (hex_notify_timeout), TYPE_INT},
|
||||
@@ -778,6 +780,8 @@ load_default_config(void)
|
||||
prefs.hex_gui_hide_menu = 1;
|
||||
#endif
|
||||
prefs.hex_gui_ctrlq_quit = 1;
|
||||
/* hex_net_remote_media stays 0: loading remote media can reveal the
|
||||
user's IP address, so it is strictly opt-in */
|
||||
prefs.hex_gui_input_attr = 1;
|
||||
prefs.hex_gui_input_icon = 1;
|
||||
prefs.hex_gui_input_nick = 1;
|
||||
|
||||
@@ -70,6 +70,9 @@ void fe_add_rawlog (struct server *serv, char *text, int len, int outbound);
|
||||
#define FE_MSG_ERROR 8
|
||||
#define FE_MSG_MARKUP 16
|
||||
void fe_message (char *msg, int flags);
|
||||
#ifdef __linux__
|
||||
void fe_root_warning (void);
|
||||
#endif
|
||||
#define FIA_READ 1
|
||||
#define FIA_WRITE 2
|
||||
#define FIA_EX 4
|
||||
|
||||
@@ -858,8 +858,10 @@ path_depth_from_root (const char *base, const char *path)
|
||||
static gint
|
||||
theme_root_candidate_compare (gconstpointer a, gconstpointer b)
|
||||
{
|
||||
const ThemeRootCandidate *ca = a;
|
||||
const ThemeRootCandidate *cb = b;
|
||||
/* g_ptr_array_sort() passes pointers to the array slots, not the values
|
||||
* stored in those slots. Dereference once before comparing candidates. */
|
||||
const ThemeRootCandidate *ca = *(ThemeRootCandidate * const *) a;
|
||||
const ThemeRootCandidate *cb = *(ThemeRootCandidate * const *) b;
|
||||
if (ca->has_index_theme != cb->has_index_theme)
|
||||
return ca->has_index_theme ? -1 : 1;
|
||||
if (ca->depth != cb->depth)
|
||||
|
||||
+74
-4
@@ -41,6 +41,8 @@
|
||||
#include "network.h"
|
||||
#include "notify.h"
|
||||
#include "outbound.h"
|
||||
#include <gmodule.h>
|
||||
#include "plugin.h"
|
||||
#include "inbound.h"
|
||||
#include "server.h"
|
||||
#include "servlist.h"
|
||||
@@ -1962,10 +1964,58 @@ inbound_cap_ack (server *serv, char *nick, char *extensions,
|
||||
inbound_toggle_caps (serv, extensions, TRUE);
|
||||
}
|
||||
|
||||
/* Split requests only at token boundaries, including plugin-added names. */
|
||||
static void
|
||||
inbound_cap_request_append (server *serv, char *buffer, gsize size,
|
||||
const char *name, time_t timestamp)
|
||||
{
|
||||
if (strlen (buffer) + strlen (name) + 1 >= size)
|
||||
{
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_CAPREQ, serv->server_session,
|
||||
buffer + 9, NULL, NULL, NULL, 0, timestamp);
|
||||
tcp_sendf (serv, "%s\r\n", g_strchomp (buffer));
|
||||
g_strlcpy (buffer, "CAP REQ :", size);
|
||||
}
|
||||
g_strlcat (buffer, name, size);
|
||||
g_strlcat (buffer, " ", size);
|
||||
}
|
||||
|
||||
static void
|
||||
inbound_plugin_cap_new (server *serv, const char *extensions, time_t timestamp)
|
||||
{
|
||||
char buffer[500] = "CAP REQ :";
|
||||
char **tokens = g_strsplit (extensions, " ", 0);
|
||||
GHashTable *seen = g_hash_table_new (g_str_hash, g_str_equal);
|
||||
int i;
|
||||
gboolean requested = FALSE;
|
||||
for (i = 0; tokens[i]; i++)
|
||||
{
|
||||
char *value = strchr (tokens[i], '=');
|
||||
if (value)
|
||||
*value = '\0';
|
||||
if (plugin_requests_capability (serv, tokens[i]) &&
|
||||
!g_hash_table_contains (seen, tokens[i]))
|
||||
{
|
||||
g_hash_table_add (seen, tokens[i]);
|
||||
inbound_cap_request_append (serv, buffer, sizeof (buffer), tokens[i], timestamp);
|
||||
requested = TRUE;
|
||||
}
|
||||
}
|
||||
if (requested)
|
||||
{
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_CAPREQ, serv->server_session,
|
||||
buffer + 9, NULL, NULL, NULL, 0, timestamp);
|
||||
tcp_sendf (serv, "%s\r\n", g_strchomp (buffer));
|
||||
}
|
||||
g_hash_table_destroy (seen);
|
||||
g_strfreev (tokens);
|
||||
}
|
||||
|
||||
void
|
||||
inbound_cap_new (server *serv, char *nick, char *extensions,
|
||||
const message_tags_data *tags_data)
|
||||
{
|
||||
gboolean sts_upgrade_triggered = FALSE;
|
||||
if (extensions)
|
||||
{
|
||||
char **tokens = g_strsplit (extensions, " ", 0);
|
||||
@@ -1977,7 +2027,7 @@ inbound_cap_new (server *serv, char *nick, char *extensions,
|
||||
|
||||
if (!g_strcmp0 (parts[0], "sts") && parts[1] && parts[1][0])
|
||||
{
|
||||
sts_handle_capability (serv, parts[1]);
|
||||
sts_upgrade_triggered |= sts_handle_capability (serv, parts[1]);
|
||||
}
|
||||
|
||||
g_strfreev (parts);
|
||||
@@ -1986,6 +2036,9 @@ inbound_cap_new (server *serv, char *nick, char *extensions,
|
||||
g_strfreev (tokens);
|
||||
}
|
||||
|
||||
if (extensions && !sts_upgrade_triggered)
|
||||
inbound_plugin_cap_new (serv, extensions, tags_data->timestamp);
|
||||
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_CAPACK, serv->server_session, nick, extensions,
|
||||
NULL, NULL, 0, tags_data->timestamp);
|
||||
|
||||
@@ -2116,6 +2169,7 @@ inbound_cap_ls (server *serv, char *nick, char *extensions_str,
|
||||
gboolean want_cap = FALSE; /* format the CAP REQ string based on previous capabilities being requested or not */
|
||||
gboolean sts_upgrade_triggered = FALSE;
|
||||
char **extensions;
|
||||
GString *requests = g_string_new ("");
|
||||
int i;
|
||||
|
||||
if (g_str_has_prefix (extensions_str, "* "))
|
||||
@@ -2181,7 +2235,7 @@ inbound_cap_ls (server *serv, char *nick, char *extensions_str,
|
||||
}
|
||||
want_cap = TRUE;
|
||||
serv->waiting_on_sasl = TRUE;
|
||||
g_strlcat (buffer, "sasl ", sizeof(buffer));
|
||||
g_string_append (requests, "sasl ");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -2189,28 +2243,44 @@ inbound_cap_ls (server *serv, char *nick, char *extensions_str,
|
||||
{
|
||||
if (!g_strcmp0 (extension, supported_caps[x]))
|
||||
{
|
||||
g_strlcat (buffer, extension, sizeof(buffer));
|
||||
g_strlcat (buffer, " ", sizeof(buffer));
|
||||
g_string_append (requests, extension);
|
||||
g_string_append_c (requests, ' ');
|
||||
want_cap = TRUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (x == G_N_ELEMENTS (supported_caps) && plugin_requests_capability (serv, extension))
|
||||
{
|
||||
g_string_append (requests, extension);
|
||||
g_string_append_c (requests, ' ');
|
||||
want_cap = TRUE;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
g_strfreev (extensions);
|
||||
|
||||
if (sts_upgrade_triggered)
|
||||
{
|
||||
g_string_free (requests, TRUE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (want_cap)
|
||||
{
|
||||
char **tokens = g_strsplit (requests->str, " ", 0);
|
||||
/* Defer every batch until STS has decided whether to reconnect. */
|
||||
for (i = 0; tokens[i]; i++)
|
||||
if (tokens[i][0])
|
||||
inbound_cap_request_append (serv, buffer, sizeof (buffer), tokens[i], tags_data->timestamp);
|
||||
g_strfreev (tokens);
|
||||
/* buffer + 9 = emit buffer without "CAP REQ :" */
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_CAPREQ, serv->server_session,
|
||||
buffer + 9, NULL, NULL, NULL, 0,
|
||||
tags_data->timestamp);
|
||||
tcp_sendf (serv, "%s\r\n", g_strchomp (buffer));
|
||||
}
|
||||
g_string_free (requests, TRUE);
|
||||
if (!serv->waiting_on_sasl && !serv->waiting_on_cap)
|
||||
{
|
||||
/* if we use SASL, CAP END is dealt via raw numerics */
|
||||
|
||||
@@ -2826,6 +2826,7 @@ static gboolean
|
||||
client_tag_allowed (server *serv, const char *tag)
|
||||
{
|
||||
char **deny;
|
||||
gboolean blocked = FALSE;
|
||||
int i;
|
||||
|
||||
if (!serv->have_message_tags)
|
||||
@@ -2837,15 +2838,16 @@ client_tag_allowed (server *serv, const char *tag)
|
||||
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;
|
||||
}
|
||||
if (!strcmp (deny[i], "*"))
|
||||
blocked = TRUE;
|
||||
else if (deny[i][0] == '-' && !strcmp (deny[i] + 1, tag))
|
||||
blocked = FALSE;
|
||||
else if (!strcmp (deny[i], tag) || (deny[i][0] == '+' && !strcmp (deny[i] + 1, tag)))
|
||||
blocked = TRUE;
|
||||
}
|
||||
|
||||
g_strfreev (deny);
|
||||
return TRUE;
|
||||
return !blocked;
|
||||
}
|
||||
|
||||
static char *
|
||||
|
||||
+149
-2
@@ -99,6 +99,10 @@ struct _zoitechat_hook
|
||||
int tag; /* for timers & FDs only */
|
||||
int type; /* HOOK_* */
|
||||
int pri; /* fd */ /* priority / fd for HOOK_FD only */
|
||||
int cap_connection_id;
|
||||
int connection_filter;
|
||||
char *channel_filter;
|
||||
gboolean context_filtered;
|
||||
};
|
||||
|
||||
struct _zoitechat_list
|
||||
@@ -143,6 +147,8 @@ enum
|
||||
HOOK_PRINT_ATTRS = 1 << 4, /* same as above, with attributes */
|
||||
HOOK_TIMER = 1 << 5, /* timeouts */
|
||||
HOOK_FD = 1 << 6, /* sockets & fds */
|
||||
HOOK_PRINT_AFTER = 1 << 8, /* opt-in post-display observers */
|
||||
HOOK_CAPABILITY = 1 << 9, /* script-owned CAP requests */
|
||||
HOOK_DELETED = 1 << 7 /* marked for deletion */
|
||||
};
|
||||
|
||||
@@ -176,6 +182,7 @@ enum
|
||||
|
||||
GSList *plugin_list = NULL; /* export for plugingui.c */
|
||||
static GSList *hook_list = NULL;
|
||||
static unsigned int plugin_dispatch_depth;
|
||||
|
||||
extern const struct prefs vars[]; /* cfgfiles.c */
|
||||
|
||||
@@ -243,7 +250,7 @@ plugin_list_add (zoitechat_context *ctx, char *filename, const char *name,
|
||||
{
|
||||
zoitechat_plugin *pl;
|
||||
|
||||
pl = g_new (zoitechat_plugin, 1);
|
||||
pl = g_new0 (zoitechat_plugin, 1);
|
||||
pl->handle = handle;
|
||||
pl->filename = filename;
|
||||
pl->context = ctx;
|
||||
@@ -350,6 +357,10 @@ plugin_add (session *sess, char *filename, void *handle, void *init_func,
|
||||
pl->zoitechat_emit_print_attrs = zoitechat_emit_print_attrs;
|
||||
pl->zoitechat_event_attrs_create = zoitechat_event_attrs_create;
|
||||
pl->zoitechat_event_attrs_free = zoitechat_event_attrs_free;
|
||||
pl->zoitechat_register_capability = zoitechat_register_capability;
|
||||
pl->zoitechat_hook_server_filtered = zoitechat_hook_server_filtered;
|
||||
pl->zoitechat_hook_print_filtered = zoitechat_hook_print_filtered;
|
||||
pl->zoitechat_hook_print_after = zoitechat_hook_print_after;
|
||||
|
||||
/* run zoitechat_plugin_init, if it returns 0, close the plugin */
|
||||
if (((zoitechat_init_func *)init_func) (pl, &pl->name, &pl->desc, &pl->version, arg) == 0)
|
||||
@@ -610,6 +621,7 @@ plugin_hook_run (session *sess, char *name, char *word[], char *word_eol[],
|
||||
zoitechat_hook *hook;
|
||||
int ret, eat = 0;
|
||||
|
||||
plugin_dispatch_depth++;
|
||||
list = hook_list;
|
||||
while (1)
|
||||
{
|
||||
@@ -619,6 +631,22 @@ plugin_hook_run (session *sess, char *name, char *word[], char *word_eol[],
|
||||
|
||||
hook = list->data;
|
||||
next = list->next;
|
||||
if (hook->context_filtered &&
|
||||
(!is_session (sess) ||
|
||||
(hook->connection_filter != -1 && hook->connection_filter != sess->server->id) ||
|
||||
(hook->channel_filter && sess->server->p_cmp (hook->channel_filter, sess->channel))))
|
||||
{
|
||||
list = next;
|
||||
continue; /* filter in C before entering an interpreter */
|
||||
}
|
||||
|
||||
if (type == HOOK_PRINT_AFTER && hook->tag)
|
||||
{
|
||||
list = next;
|
||||
continue; /* no recursive invocation of this same observer */
|
||||
}
|
||||
if (type == HOOK_PRINT_AFTER)
|
||||
hook->tag = 1;
|
||||
hook->pl->context = sess;
|
||||
|
||||
/* run the plugin's callback function */
|
||||
@@ -627,6 +655,7 @@ plugin_hook_run (session *sess, char *name, char *word[], char *word_eol[],
|
||||
case HOOK_COMMAND:
|
||||
ret = ((zoitechat_cmd_cb *)hook->callback) (word, word_eol, hook->userdata);
|
||||
break;
|
||||
case HOOK_PRINT_AFTER:
|
||||
case HOOK_PRINT_ATTRS:
|
||||
ret = ((zoitechat_print_attrs_cb *)hook->callback) (word, attrs, hook->userdata);
|
||||
break;
|
||||
@@ -641,6 +670,15 @@ plugin_hook_run (session *sess, char *name, char *word[], char *word_eol[],
|
||||
break;
|
||||
}
|
||||
|
||||
if (type == HOOK_PRINT_AFTER)
|
||||
{
|
||||
hook->tag = 0;
|
||||
if (!is_session (sess))
|
||||
goto xit;
|
||||
list = next;
|
||||
continue; /* display already happened; observers cannot eat it */
|
||||
}
|
||||
|
||||
if ((ret & ZOITECHAT_EAT_ZOITECHAT) && (ret & ZOITECHAT_EAT_PLUGIN))
|
||||
{
|
||||
eat = 1;
|
||||
@@ -655,6 +693,10 @@ plugin_hook_run (session *sess, char *name, char *word[], char *word_eol[],
|
||||
}
|
||||
|
||||
xit:
|
||||
/* Nested callbacks can unhook entries still used by an outer dispatch.
|
||||
* Keep both hook objects and list links alive until the outermost return. */
|
||||
if (--plugin_dispatch_depth != 0)
|
||||
return eat;
|
||||
/* really remove deleted hooks now */
|
||||
list = hook_list;
|
||||
while (list)
|
||||
@@ -719,6 +761,15 @@ plugin_emit_print (session *sess, char *word[], time_t server_time)
|
||||
HOOK_PRINT | HOOK_PRINT_ATTRS);
|
||||
}
|
||||
|
||||
void
|
||||
plugin_emit_print_after (session *sess, char *word[], time_t server_time)
|
||||
{
|
||||
zoitechat_event_attrs attrs;
|
||||
attrs.server_time_utc = server_time;
|
||||
if (is_session (sess))
|
||||
plugin_hook_run (sess, word[0], word, NULL, &attrs, HOOK_PRINT_AFTER);
|
||||
}
|
||||
|
||||
int
|
||||
plugin_emit_dummy_print (session *sess, char *name)
|
||||
{
|
||||
@@ -806,7 +857,7 @@ plugin_insert_hook (zoitechat_hook *new_hook)
|
||||
break;
|
||||
case HOOK_SERVER:
|
||||
case HOOK_SERVER_ATTRS:
|
||||
new_hook_type = HOOK_SERVER | HOOK_PRINT_ATTRS;
|
||||
new_hook_type = HOOK_SERVER | HOOK_SERVER_ATTRS;
|
||||
break;
|
||||
default:
|
||||
new_hook_type = new_hook->type;
|
||||
@@ -1018,10 +1069,87 @@ zoitechat_unhook (zoitechat_plugin *ph, zoitechat_hook *hook)
|
||||
|
||||
g_free (hook->name); /* NULL for timers & fds */
|
||||
g_free (hook->help_text); /* NULL for non-commands */
|
||||
g_free (hook->channel_filter);
|
||||
hook->channel_filter = NULL;
|
||||
|
||||
return hook->userdata;
|
||||
}
|
||||
|
||||
zoitechat_hook *
|
||||
zoitechat_hook_print_after (zoitechat_plugin *ph, const char *name, int pri,
|
||||
int flags, zoitechat_print_attrs_cb *callback, void *userdata)
|
||||
{
|
||||
if (flags != 0 || !name || !callback)
|
||||
return NULL;
|
||||
return plugin_add_hook (ph, HOOK_PRINT_AFTER, pri, name, NULL, callback, 0, userdata);
|
||||
}
|
||||
|
||||
static zoitechat_hook *
|
||||
plugin_add_filtered_hook (zoitechat_plugin *ph, const char *name, int pri,
|
||||
int flags, int connection_id, const char *channel,
|
||||
int type, void *callback, void *userdata)
|
||||
{
|
||||
zoitechat_hook *hook;
|
||||
if (flags != 0 || connection_id < -1 || !name || !callback)
|
||||
return NULL;
|
||||
hook = plugin_add_hook (ph, type, pri, name, NULL, callback, 0, userdata);
|
||||
hook->context_filtered = TRUE;
|
||||
hook->connection_filter = connection_id;
|
||||
hook->channel_filter = g_strdup (channel);
|
||||
return hook;
|
||||
}
|
||||
|
||||
zoitechat_hook *
|
||||
zoitechat_hook_server_filtered (zoitechat_plugin *ph, const char *name, int pri,
|
||||
int flags, int connection_id, const char *channel,
|
||||
zoitechat_serv_attrs_cb *callback, void *userdata)
|
||||
{
|
||||
return plugin_add_filtered_hook (ph, name, pri, flags, connection_id, channel,
|
||||
HOOK_SERVER_ATTRS, callback, userdata);
|
||||
}
|
||||
|
||||
zoitechat_hook *
|
||||
zoitechat_hook_print_filtered (zoitechat_plugin *ph, const char *name, int pri,
|
||||
int flags, int connection_id, const char *channel,
|
||||
zoitechat_print_attrs_cb *callback, void *userdata)
|
||||
{
|
||||
return plugin_add_filtered_hook (ph, name, pri, flags, connection_id, channel,
|
||||
HOOK_PRINT_ATTRS, callback, userdata);
|
||||
}
|
||||
|
||||
zoitechat_hook *
|
||||
zoitechat_register_capability (zoitechat_plugin *ph, const char *name, int flags,
|
||||
int connection_id, void *userdata)
|
||||
{
|
||||
const unsigned char *p;
|
||||
zoitechat_hook *hook;
|
||||
if (flags != 0 || connection_id < -1 || !name || !name[0] || strlen (name) > 200 ||
|
||||
strcmp (name, "sasl") == 0 || strcmp (name, "sts") == 0)
|
||||
return NULL; /* authentication and transport policy belong to core */
|
||||
for (p = (const unsigned char *)name; *p; p++)
|
||||
if (!g_ascii_islower (*p) && !g_ascii_isdigit (*p) &&
|
||||
*p != '-' && *p != '.' && *p != '/' && *p != '_')
|
||||
return NULL;
|
||||
hook = plugin_add_hook (ph, HOOK_CAPABILITY, 0, name, NULL, NULL, 0, userdata);
|
||||
hook->cap_connection_id = connection_id;
|
||||
return hook;
|
||||
}
|
||||
|
||||
gboolean
|
||||
plugin_requests_capability (server *serv, const char *name)
|
||||
{
|
||||
GSList *entry;
|
||||
for (entry = hook_list; entry; entry = entry->next)
|
||||
{
|
||||
zoitechat_hook *hook = entry->data;
|
||||
if (hook && hook->type == HOOK_CAPABILITY &&
|
||||
(hook->cap_connection_id == -1 || hook->cap_connection_id == serv->id) &&
|
||||
strcmp (hook->name, name) == 0)
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
zoitechat_hook *
|
||||
zoitechat_hook_command (zoitechat_plugin *ph, const char *name, int pri,
|
||||
zoitechat_cmd_cb *callb, const char *help_text, void *userdata)
|
||||
@@ -1181,6 +1309,25 @@ zoitechat_get_info (zoitechat_plugin *ph, const char *id)
|
||||
return text_find_format_string (e);
|
||||
}
|
||||
|
||||
/* Report the comparison actually used by this connection, not an
|
||||
* advertised mapping that the current core does not implement. */
|
||||
if (strcmp (id, "casemapping") == 0)
|
||||
{
|
||||
if (!is_session (ph->context))
|
||||
return NULL;
|
||||
return ph->context->server->p_cmp == g_ascii_strcasecmp
|
||||
? "ascii" : "rfc1459";
|
||||
}
|
||||
|
||||
if (strcmp (id, "api_hook_print_after") == 0)
|
||||
return "1";
|
||||
|
||||
if (strcmp (id, "api_context_filters") == 0)
|
||||
return "1";
|
||||
|
||||
if (strcmp (id, "api_plugin_caps") == 0)
|
||||
return "1";
|
||||
|
||||
hash = str_hash (id);
|
||||
/* do the session independant ones first */
|
||||
switch (hash)
|
||||
|
||||
@@ -150,6 +150,15 @@ struct _zoitechat_plugin
|
||||
void (*zoitechat_event_attrs_free) (zoitechat_plugin *ph,
|
||||
zoitechat_event_attrs *attrs);
|
||||
|
||||
/* Additive API slots: append only; old binary offsets are unchanged. */
|
||||
zoitechat_hook *(*zoitechat_hook_print_after) (zoitechat_plugin *, const char *, int,
|
||||
int, int (*)(char *[], zoitechat_event_attrs *, void *), void *);
|
||||
zoitechat_hook *(*zoitechat_hook_server_filtered) (zoitechat_plugin *, const char *, int,
|
||||
int, int, const char *, int (*)(char *[], char *[], zoitechat_event_attrs *, void *), void *);
|
||||
zoitechat_hook *(*zoitechat_hook_print_filtered) (zoitechat_plugin *, const char *, int,
|
||||
int, int, const char *, int (*)(char *[], zoitechat_event_attrs *, void *), void *);
|
||||
zoitechat_hook *(*zoitechat_register_capability) (zoitechat_plugin *, const char *, int, int, void *);
|
||||
|
||||
/* PRIVATE FIELDS! */
|
||||
void *handle; /* from dlopen */
|
||||
char *filename; /* loaded from */
|
||||
@@ -174,6 +183,8 @@ int plugin_emit_command (session *sess, char *name, char *word[], char *word_eol
|
||||
int plugin_emit_server (session *sess, char *name, char *word[], char *word_eol[],
|
||||
time_t server_time);
|
||||
int plugin_emit_print (session *sess, char *word[], time_t server_time);
|
||||
gboolean plugin_requests_capability (server *serv, const char *name);
|
||||
void plugin_emit_print_after (session *sess, char *word[], time_t server_time);
|
||||
int plugin_emit_dummy_print (session *sess, char *name);
|
||||
int plugin_emit_keypress (session *sess, unsigned int state, unsigned int keyval, gunichar key);
|
||||
GList* plugin_command_list(GList *tmp_list);
|
||||
|
||||
@@ -2306,6 +2306,7 @@ text_emit (int index, session *sess, char *a, char *b, char *c, char *d,
|
||||
if (!prefs.hex_away_omit_alerts || !sess->server->is_away)
|
||||
sound_play_event (index);
|
||||
display_event (sess, index, word, stripcolor_args, timestamp);
|
||||
plugin_emit_print_after (sess, word, timestamp);
|
||||
}
|
||||
|
||||
char *
|
||||
|
||||
@@ -193,10 +193,39 @@ struct _zoitechat_plugin
|
||||
zoitechat_event_attrs *(*zoitechat_event_attrs_create) (zoitechat_plugin *ph);
|
||||
void (*zoitechat_event_attrs_free) (zoitechat_plugin *ph,
|
||||
zoitechat_event_attrs *attrs);
|
||||
/* Additive API slots: append only; old binary offsets are unchanged. */
|
||||
zoitechat_hook *(*zoitechat_hook_print_after) (zoitechat_plugin *, const char *, int,
|
||||
int, int (*)(char *[], zoitechat_event_attrs *, void *), void *);
|
||||
zoitechat_hook *(*zoitechat_hook_server_filtered) (zoitechat_plugin *, const char *, int,
|
||||
int, int, const char *, int (*)(char *[], char *[], zoitechat_event_attrs *, void *), void *);
|
||||
zoitechat_hook *(*zoitechat_hook_print_filtered) (zoitechat_plugin *, const char *, int,
|
||||
int, int, const char *, int (*)(char *[], zoitechat_event_attrs *, void *), void *);
|
||||
zoitechat_hook *(*zoitechat_register_capability) (zoitechat_plugin *, const char *, int, int, void *);
|
||||
|
||||
};
|
||||
#endif
|
||||
|
||||
|
||||
/* api_hook_print_after=1: observer returns are ignored; flags must be zero. */
|
||||
zoitechat_hook *zoitechat_hook_print_after (zoitechat_plugin *ph,
|
||||
const char *name, int pri, int flags,
|
||||
int (*callback) (char *word[], zoitechat_event_attrs *attrs, void *userdata),
|
||||
void *userdata);
|
||||
|
||||
/* api_context_filters=1: connection_id -1 is any; NULL channel is any. */
|
||||
zoitechat_hook *zoitechat_hook_server_filtered (zoitechat_plugin *ph,
|
||||
const char *name, int pri, int flags, int connection_id, const char *channel,
|
||||
int (*callback) (char *word[], char *word_eol[], zoitechat_event_attrs *attrs, void *userdata),
|
||||
void *userdata);
|
||||
zoitechat_hook *zoitechat_hook_print_filtered (zoitechat_plugin *ph,
|
||||
const char *name, int pri, int flags, int connection_id, const char *channel,
|
||||
int (*callback) (char *word[], zoitechat_event_attrs *attrs, void *userdata),
|
||||
void *userdata);
|
||||
|
||||
/* api_plugin_caps=1: register before CAP LS; flags=0, connection_id=-1 for any. */
|
||||
zoitechat_hook *zoitechat_register_capability (zoitechat_plugin *ph,
|
||||
const char *name, int flags, int connection_id, void *userdata);
|
||||
|
||||
zoitechat_hook *
|
||||
zoitechat_hook_command (zoitechat_plugin *ph,
|
||||
const char *name,
|
||||
@@ -416,6 +445,10 @@ zoitechat_pluginpref_list (zoitechat_plugin *ph,
|
||||
#define zoitechat_hook_server ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_server)
|
||||
#define zoitechat_hook_server_attrs ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_server_attrs)
|
||||
#define zoitechat_hook_print ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_print)
|
||||
#define zoitechat_register_capability ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_register_capability)
|
||||
#define zoitechat_hook_server_filtered ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_server_filtered)
|
||||
#define zoitechat_hook_print_filtered ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_print_filtered)
|
||||
#define zoitechat_hook_print_after ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_print_after)
|
||||
#define zoitechat_hook_print_attrs ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_print_attrs)
|
||||
#define zoitechat_hook_timer ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_timer)
|
||||
#define zoitechat_hook_fd ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_fd)
|
||||
|
||||
+22
-3
@@ -95,6 +95,7 @@ int zoitechat_is_quitting = FALSE;
|
||||
/* command-line args */
|
||||
int arg_dont_autoconnect = FALSE;
|
||||
int arg_skip_plugins = FALSE;
|
||||
gint arg_run_as_root = FALSE;
|
||||
char *arg_url = NULL;
|
||||
char **arg_urls = NULL;
|
||||
char *arg_command = NULL;
|
||||
@@ -463,8 +464,7 @@ doover:
|
||||
while (list)
|
||||
{
|
||||
sess = list->data;
|
||||
if (!sess->server->have_awaynotify)
|
||||
sess->done_away_check = FALSE;
|
||||
sess->done_away_check = FALSE;
|
||||
list = list->next;
|
||||
}
|
||||
loop++;
|
||||
@@ -1176,6 +1176,25 @@ main (int argc, char *argv[])
|
||||
HRESULT coinit_result;
|
||||
#endif
|
||||
|
||||
#ifdef __linux__
|
||||
for (i = 1; i < argc; i++)
|
||||
{
|
||||
if (strcmp (argv[i], "--") == 0)
|
||||
break;
|
||||
if (strcmp (argv[i], "--run-as-root") == 0)
|
||||
{
|
||||
arg_run_as_root = TRUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (geteuid () == 0 && !arg_run_as_root)
|
||||
{
|
||||
fe_root_warning ();
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
#endif
|
||||
|
||||
srand ((unsigned int) time (NULL)); /* CL: do this only once! */
|
||||
|
||||
/* We must check for the config dir parameter, otherwise load_config() will behave incorrectly.
|
||||
@@ -1264,7 +1283,7 @@ main (int argc, char *argv[])
|
||||
#ifndef WIN32
|
||||
#ifndef __EMX__
|
||||
/* OS/2 uses UID 0 all the time */
|
||||
if (getuid () == 0)
|
||||
if (getuid () == 0 && !arg_run_as_root)
|
||||
fe_message (_("* Running IRC as root is stupid! You should\n"
|
||||
" create a User Account and use that to login.\n"), FE_MSG_WARN|FE_MSG_WAIT);
|
||||
#endif
|
||||
|
||||
@@ -204,6 +204,7 @@ struct zoitechatprefs
|
||||
unsigned int hex_net_auto_reconnect;
|
||||
unsigned int hex_net_auto_reconnectonfail;
|
||||
unsigned int hex_net_proxy_auth;
|
||||
unsigned int hex_net_remote_media;
|
||||
unsigned int hex_net_throttle;
|
||||
unsigned int hex_notify_whois_online;
|
||||
unsigned int hex_perl_warnings;
|
||||
@@ -314,6 +315,7 @@ struct zoitechatprefs
|
||||
char hex_dcc_completed_dir[PATHLEN + 1];
|
||||
char hex_dcc_dir[PATHLEN + 1];
|
||||
char hex_dcc_ip[DOMAINLEN + 1];
|
||||
char hex_gui_tab_font[FONTNAMELEN + 1];
|
||||
char hex_gui_ulist_doubleclick[256];
|
||||
char hex_input_command_char[4];
|
||||
char hex_irc_extra_hilight[300];
|
||||
@@ -448,6 +450,7 @@ typedef struct session
|
||||
int mode_timeout_tag;
|
||||
int typing_timeout_tag;
|
||||
int typing_status;
|
||||
gint64 typing_last_sent;
|
||||
int typing_animation_tag;
|
||||
int typing_animation_frame;
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ extern struct zoitechatprefs prefs;
|
||||
extern int zoitechat_is_quitting;
|
||||
extern gint arg_skip_plugins; /* command-line args */
|
||||
extern gint arg_dont_autoconnect;
|
||||
extern gint arg_run_as_root;
|
||||
extern char *arg_url;
|
||||
extern char **arg_urls;
|
||||
extern char *arg_command;
|
||||
|
||||
+3
-3
@@ -138,7 +138,7 @@ ascii_open (void)
|
||||
{
|
||||
table_pos++;
|
||||
hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
gtk_container_add (GTK_CONTAINER (vbox), hbox);
|
||||
gtk_box_pack_start (GTK_BOX (vbox), hbox, TRUE, TRUE, 0);
|
||||
gtk_widget_show (hbox);
|
||||
i++;
|
||||
continue;
|
||||
@@ -155,14 +155,14 @@ ascii_open (void)
|
||||
G_CALLBACK (ascii_click), NULL);
|
||||
g_signal_connect (G_OBJECT (but), "enter-notify-event",
|
||||
G_CALLBACK (ascii_enter), label);
|
||||
gtk_box_pack_start (GTK_BOX (hbox), but, 0, 0, 0);
|
||||
gtk_box_pack_start (GTK_BOX (hbox), but, TRUE, TRUE, 0);
|
||||
gtk_widget_show (but);
|
||||
|
||||
table_pos += len;
|
||||
}
|
||||
|
||||
frame = gtk_frame_new ("");
|
||||
gtk_container_add (GTK_CONTAINER (hbox), frame);
|
||||
gtk_box_pack_start (GTK_BOX (hbox), frame, TRUE, TRUE, 0);
|
||||
gtk_container_add (GTK_CONTAINER (frame), label);
|
||||
gtk_widget_show (label);
|
||||
gtk_widget_show (frame);
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
/* file included in chanview.c */
|
||||
|
||||
#include "theme/theme-css.h"
|
||||
|
||||
typedef struct
|
||||
{
|
||||
GtkWidget *outer; /* outer box */
|
||||
@@ -690,6 +692,7 @@ cv_tabs_add (chanview *cv, chan *ch, char *name, GtkTreeIter *parent)
|
||||
GtkWidget *label;
|
||||
GtkWidget *close_button;
|
||||
GtkWidget *close_icon;
|
||||
GtkCssProvider *close_css;
|
||||
|
||||
but = gtk_toggle_button_new ();
|
||||
gtk_widget_set_name (but, "zoitechat-tab");
|
||||
@@ -698,9 +701,27 @@ cv_tabs_add (chanview *cv, chan *ch, char *name, GtkTreeIter *parent)
|
||||
cv_add_scroll_events (but);
|
||||
hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 2);
|
||||
label = gtk_label_new (name);
|
||||
if (prefs.hex_gui_tab_font[0])
|
||||
{
|
||||
PangoFontDescription *font_desc;
|
||||
|
||||
/* Keep activity colours and the Smaller text scale in the label attributes. */
|
||||
font_desc = pango_font_description_from_string (prefs.hex_gui_tab_font);
|
||||
gtkutil_apply_palette (label, NULL, NULL, font_desc);
|
||||
pango_font_description_free (font_desc);
|
||||
}
|
||||
close_button = gtk_button_new ();
|
||||
cv_add_scroll_events (close_button);
|
||||
gtk_style_context_add_class (gtk_widget_get_style_context (close_button), "flat");
|
||||
gtk_style_context_add_class (gtk_widget_get_style_context (close_button), "zoitechat-tab-close");
|
||||
/* Theme button minimums and padding must not determine the tab height. */
|
||||
close_css = gtk_css_provider_new ();
|
||||
gtk_css_provider_load_from_data (close_css,
|
||||
".zoitechat-tab-close { min-width: 0; min-height: 0; padding: 1px; margin: 0; }",
|
||||
-1, NULL);
|
||||
theme_css_apply_widget_provider (close_button, GTK_STYLE_PROVIDER (close_css));
|
||||
g_object_unref (close_css);
|
||||
gtk_widget_set_valign (close_button, GTK_ALIGN_CENTER);
|
||||
close_icon = gtk_image_new_from_icon_name ("window-close-symbolic", GTK_ICON_SIZE_MENU);
|
||||
gtk_image_set_pixel_size (GTK_IMAGE (close_icon), 8);
|
||||
gtk_button_set_always_show_image (GTK_BUTTON (close_button), TRUE);
|
||||
|
||||
+28
-19
@@ -301,26 +301,35 @@ cv_tree_focus (chan *ch)
|
||||
path = gtk_tree_model_get_path (model, &ch->iter);
|
||||
if (path)
|
||||
{
|
||||
/* This full section does what
|
||||
* gtk_tree_view_scroll_to_cell (tree, path, NULL, TRUE, 0.5, 0.5);
|
||||
* does, except it only scrolls the window if the provided cell is
|
||||
* not visible. Basic algorithm taken from gtktreeview.c */
|
||||
|
||||
/* obtain information to see if the cell is visible */
|
||||
gtk_tree_view_get_background_area (tree, path, NULL, &cell_rect);
|
||||
gtk_tree_view_get_visible_rect (tree, &vis_rect);
|
||||
|
||||
/* The cordinates aren't offset correctly */
|
||||
gtk_tree_view_convert_widget_to_bin_window_coords ( tree, cell_rect.x, cell_rect.y, NULL, &cell_rect.y );
|
||||
|
||||
/* only need to scroll if out of bounds */
|
||||
if (cell_rect.y < vis_rect.y ||
|
||||
cell_rect.y + cell_rect.height > vis_rect.y + vis_rect.height)
|
||||
if (!gtk_widget_get_realized (GTK_WIDGET (tree)))
|
||||
{
|
||||
dest_y = cell_rect.y - ((vis_rect.height - cell_rect.height) * 0.5);
|
||||
if (dest_y < 0)
|
||||
dest_y = 0;
|
||||
gtk_tree_view_scroll_to_point (tree, -1, dest_y);
|
||||
/* Layout changes can restore focus before the new tree is realized.
|
||||
* Let GTK track the row and scroll once its geometry is available. */
|
||||
gtk_tree_view_scroll_to_cell (tree, path, NULL, TRUE, 0.5, 0.5);
|
||||
}
|
||||
else
|
||||
{
|
||||
/* This full section does what
|
||||
* gtk_tree_view_scroll_to_cell (tree, path, NULL, TRUE, 0.5, 0.5);
|
||||
* does, except it only scrolls the window if the provided cell is
|
||||
* not visible. Basic algorithm taken from gtktreeview.c */
|
||||
|
||||
/* obtain information to see if the cell is visible */
|
||||
gtk_tree_view_get_background_area (tree, path, NULL, &cell_rect);
|
||||
gtk_tree_view_get_visible_rect (tree, &vis_rect);
|
||||
|
||||
/* The cordinates aren't offset correctly */
|
||||
gtk_tree_view_convert_widget_to_bin_window_coords ( tree, cell_rect.x, cell_rect.y, NULL, &cell_rect.y );
|
||||
|
||||
/* only need to scroll if out of bounds */
|
||||
if (cell_rect.y < vis_rect.y ||
|
||||
cell_rect.y + cell_rect.height > vis_rect.y + vis_rect.height)
|
||||
{
|
||||
dest_y = cell_rect.y - ((vis_rect.height - cell_rect.height) * 0.5);
|
||||
if (dest_y < 0)
|
||||
dest_y = 0;
|
||||
gtk_tree_view_scroll_to_point (tree, -1, dest_y);
|
||||
}
|
||||
}
|
||||
/* theft done, now make it focused like */
|
||||
gtk_tree_view_set_cursor (tree, path, NULL, FALSE);
|
||||
|
||||
+38
-1
@@ -49,6 +49,7 @@
|
||||
#include "chanlist.h"
|
||||
#include "joind.h"
|
||||
#include "xtext.h"
|
||||
#include "inline-image.h"
|
||||
#include "theme/theme-gtk.h"
|
||||
#include "menu.h"
|
||||
#include "notifygui.h"
|
||||
@@ -89,6 +90,9 @@ static const GOptionEntry gopt_entries[] =
|
||||
{"no-auto", 'a', 0, G_OPTION_ARG_NONE, &arg_dont_autoconnect, N_("Don't auto connect to servers"), NULL},
|
||||
{"cfgdir", 'd', 0, G_OPTION_ARG_STRING, &arg_cfgdir, N_("Use a different config directory"), "PATH"},
|
||||
{"no-plugins", 'n', 0, G_OPTION_ARG_NONE, &arg_skip_plugins, N_("Don't auto load any plugins"), NULL},
|
||||
#ifdef __linux__
|
||||
{"run-as-root", 0, 0, G_OPTION_ARG_NONE, &arg_run_as_root, N_("Allow ZoiteChat to run as root (unsafe)"), NULL},
|
||||
#endif
|
||||
{"plugindir", 'p', 0, G_OPTION_ARG_NONE, &arg_show_autoload, N_("Show plugin/script auto-load directory"), NULL},
|
||||
{"configdir", 'u', 0, G_OPTION_ARG_NONE, &arg_show_config, N_("Show user config directory"), NULL},
|
||||
{"url", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &arg_url, N_("Open an irc://server:port/channel?key URL"), "URL"},
|
||||
@@ -102,6 +106,33 @@ static const GOptionEntry gopt_entries[] =
|
||||
{NULL}
|
||||
};
|
||||
|
||||
#ifdef __linux__
|
||||
void
|
||||
fe_root_warning (void)
|
||||
{
|
||||
const char *message =
|
||||
"ZoiteChat refuses to run as root on Linux.\n\n"
|
||||
"Run it as a normal user, or pass --run-as-root to override this protection.";
|
||||
GtkWidget *dialog;
|
||||
|
||||
g_printerr ("%s\n", message);
|
||||
|
||||
/* A graphical session may not be accessible to root, particularly under
|
||||
* Wayland. Keep stderr as the fallback if GTK cannot open the display. */
|
||||
if (!gtk_init_check (NULL, NULL))
|
||||
return;
|
||||
|
||||
dialog = gtk_message_dialog_new (NULL,
|
||||
GTK_DIALOG_MODAL,
|
||||
GTK_MESSAGE_ERROR,
|
||||
GTK_BUTTONS_CLOSE,
|
||||
"%s", message);
|
||||
gtk_window_set_title (GTK_WINDOW (dialog), "ZoiteChat");
|
||||
gtk_dialog_run (GTK_DIALOG (dialog));
|
||||
gtk_widget_destroy (dialog);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef WIN32
|
||||
static void
|
||||
create_msg_dialog (gchar *title, gchar *message)
|
||||
@@ -943,7 +974,13 @@ void
|
||||
fe_print_text (struct session *sess, char *text, time_t stamp,
|
||||
gboolean no_activity)
|
||||
{
|
||||
PrintTextRaw (sess->res->buffer, (unsigned char *)text, prefs.hex_text_indent, stamp);
|
||||
/* image links become a "[view image]" placeholder with a hidden URL */
|
||||
char *filtered = inline_image_filter_text (text);
|
||||
|
||||
PrintTextRaw (sess->res->buffer,
|
||||
(unsigned char *)(filtered ? filtered : text),
|
||||
prefs.hex_text_indent, stamp);
|
||||
g_free (filtered);
|
||||
|
||||
if (no_activity || !sess->gui->is_tab)
|
||||
return;
|
||||
|
||||
@@ -59,6 +59,7 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out-
|
||||
<ClInclude Include="fkeys.h" />
|
||||
<ClInclude Include="gtkutil.h" />
|
||||
<ClInclude Include="icon-resolver.h" />
|
||||
<ClInclude Include="inline-image.h" />
|
||||
<ClInclude Include="joind.h" />
|
||||
<ClInclude Include="maingui.h" />
|
||||
<ClInclude Include="preferences-persistence.h" />
|
||||
@@ -104,6 +105,7 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out-
|
||||
<ClCompile Include="gtkutil.c" />
|
||||
<ClCompile Include="icon-resolver.c" />
|
||||
<ClCompile Include="ignoregui.c" />
|
||||
<ClCompile Include="inline-image.c" />
|
||||
<ClCompile Include="joind.c" />
|
||||
<ClCompile Include="maingui.c" />
|
||||
<ClCompile Include="menu.c" />
|
||||
|
||||
@@ -45,6 +45,9 @@
|
||||
<ClInclude Include="icon-resolver.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="inline-image.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="joind.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
@@ -170,6 +173,9 @@
|
||||
<ClCompile Include="ignoregui.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="inline-image.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="joind.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
|
||||
+13
-3
@@ -1221,7 +1221,7 @@ key_dialog_load (GtkListStore *store)
|
||||
void
|
||||
key_dialog_show ()
|
||||
{
|
||||
GtkWidget *vbox, *box;
|
||||
GtkWidget *vbox, *box, *pane, *scroll;
|
||||
GtkWidget *view, *xtext, *delete_button;
|
||||
GtkListStore *store;
|
||||
XTextColor xtext_palette[XTEXT_COLS];
|
||||
@@ -1237,10 +1237,20 @@ key_dialog_show ()
|
||||
key_dialog = mg_create_generic_tab ("editkeys", buf, TRUE, FALSE, key_dialog_close,
|
||||
NULL, 600, 360, &vbox, 0);
|
||||
|
||||
view = key_dialog_treeview_new (vbox);
|
||||
pane = gtk_paned_new (GTK_ORIENTATION_VERTICAL);
|
||||
gtk_box_pack_start (GTK_BOX (vbox), pane, TRUE, TRUE, 0);
|
||||
box = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 0);
|
||||
gtk_paned_pack1 (GTK_PANED (pane), box, TRUE, FALSE);
|
||||
view = key_dialog_treeview_new (box);
|
||||
scroll = gtk_scrolled_window_new (NULL, NULL);
|
||||
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroll),
|
||||
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||
gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (scroll), 80);
|
||||
gtk_paned_pack2 (GTK_PANED (pane), scroll, TRUE, FALSE);
|
||||
gtk_paned_set_position (GTK_PANED (pane), 220);
|
||||
theme_get_xtext_colors (xtext_palette, XTEXT_COLS);
|
||||
xtext = gtk_xtext_new (xtext_palette, 0);
|
||||
gtk_box_pack_start (GTK_BOX (vbox), xtext, FALSE, TRUE, 2);
|
||||
gtk_container_add (GTK_CONTAINER (scroll), xtext);
|
||||
gtk_xtext_set_font (GTK_XTEXT (xtext), prefs.hex_text_font);
|
||||
|
||||
g_object_set_data (G_OBJECT (key_dialog), "view", view);
|
||||
|
||||
+17
-11
@@ -641,9 +641,8 @@ gtkutil_get_str (char *msg, char *def, void *callback, void *userdata, gboolean
|
||||
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);
|
||||
|
||||
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, TRUE, 0);
|
||||
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6);
|
||||
|
||||
g_object_set_data (G_OBJECT (dialog), "cb", callback);
|
||||
g_object_set_data (G_OBJECT (dialog), "ud", userdata);
|
||||
@@ -653,7 +652,8 @@ gtkutil_get_str (char *msg, char *def, void *callback, void *userdata, gboolean
|
||||
G_CALLBACK (gtkutil_str_enter), dialog);
|
||||
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);
|
||||
gtk_widget_set_valign (entry, GTK_ALIGN_CENTER);
|
||||
gtk_box_pack_end (GTK_BOX (hbox), entry, TRUE, TRUE, 0);
|
||||
|
||||
label = gtk_label_new (msg);
|
||||
gtk_box_pack_end (GTK_BOX (hbox), label, 0, 0, 0);
|
||||
@@ -661,7 +661,8 @@ gtkutil_get_str (char *msg, char *def, void *callback, void *userdata, gboolean
|
||||
g_signal_connect (G_OBJECT (dialog), "response",
|
||||
G_CALLBACK (gtkutil_get_str_response), entry);
|
||||
|
||||
gtk_container_add (GTK_CONTAINER (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), hbox);
|
||||
gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))),
|
||||
hbox, TRUE, TRUE, 0);
|
||||
|
||||
gtk_widget_show_all (dialog);
|
||||
}
|
||||
@@ -742,9 +743,8 @@ fe_get_int (char *msg, int def, void *callback, void *userdata)
|
||||
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);
|
||||
|
||||
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, TRUE, 0);
|
||||
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6);
|
||||
|
||||
g_object_set_data (G_OBJECT (dialog), "cb", callback);
|
||||
g_object_set_data (G_OBJECT (dialog), "ud", userdata);
|
||||
@@ -755,7 +755,8 @@ fe_get_int (char *msg, int def, void *callback, void *userdata)
|
||||
gtk_adjustment_set_upper (adj, 1024);
|
||||
gtk_adjustment_set_step_increment (adj, 1);
|
||||
gtk_spin_button_set_value ((GtkSpinButton*)spin, def);
|
||||
gtk_box_pack_end (GTK_BOX (hbox), spin, 0, 0, 0);
|
||||
gtk_widget_set_valign (spin, GTK_ALIGN_CENTER);
|
||||
gtk_box_pack_end (GTK_BOX (hbox), spin, TRUE, TRUE, 0);
|
||||
|
||||
label = gtk_label_new (msg);
|
||||
gtk_box_pack_end (GTK_BOX (hbox), label, 0, 0, 0);
|
||||
@@ -763,7 +764,8 @@ fe_get_int (char *msg, int def, void *callback, void *userdata)
|
||||
g_signal_connect (G_OBJECT (dialog), "response",
|
||||
G_CALLBACK (gtkutil_get_number_response), spin);
|
||||
|
||||
gtk_container_add (GTK_CONTAINER (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), hbox);
|
||||
gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))),
|
||||
hbox, TRUE, TRUE, 0);
|
||||
|
||||
gtk_widget_show_all (dialog);
|
||||
}
|
||||
@@ -783,17 +785,18 @@ fe_get_bool (char *title, char *prompt, void *callback, void *userdata)
|
||||
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);
|
||||
|
||||
g_object_set_data (G_OBJECT (dialog), "cb", callback);
|
||||
g_object_set_data (G_OBJECT (dialog), "ud", userdata);
|
||||
|
||||
prompt_label = gtk_label_new (prompt);
|
||||
gtk_label_set_line_wrap (GTK_LABEL (prompt_label), TRUE);
|
||||
|
||||
g_signal_connect (G_OBJECT (dialog), "response",
|
||||
G_CALLBACK (gtkutil_get_bool_response), NULL);
|
||||
|
||||
gtk_container_add (GTK_CONTAINER (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), prompt_label);
|
||||
gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))),
|
||||
prompt_label, TRUE, TRUE, 0);
|
||||
|
||||
gtk_widget_show_all (dialog);
|
||||
}
|
||||
@@ -885,7 +888,10 @@ gtkutil_window_new (char *title, char *role, int width, int height, int flags)
|
||||
gtk_window_set_wmclass (GTK_WINDOW (win), "ZoiteChat", "zoitechat");
|
||||
#endif
|
||||
gtk_window_set_title (GTK_WINDOW (win), title);
|
||||
gtk_window_set_default_size (GTK_WINDOW (win), width, height);
|
||||
/* Zero means content-sized to callers; GTK requires -1 for that. */
|
||||
gtk_window_set_default_size (GTK_WINDOW (win),
|
||||
width > 0 ? width : -1,
|
||||
height > 0 ? height : -1);
|
||||
gtk_window_set_role (GTK_WINDOW (win), role);
|
||||
if (flags & 1)
|
||||
gtk_window_set_position (GTK_WINDOW (win), GTK_WIN_POS_MOUSE);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
||||
/* ZoiteChat
|
||||
* Copyright (C) 2026 ZoiteChat contributors.
|
||||
*
|
||||
* 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
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
|
||||
*/
|
||||
|
||||
#ifndef ZOITECHAT_INLINE_IMAGE_H
|
||||
#define ZOITECHAT_INLINE_IMAGE_H
|
||||
|
||||
#include "xtext.h"
|
||||
|
||||
/* does this look like a direct http(s) link to an image file? */
|
||||
gboolean inline_image_is_image_url (const char *url);
|
||||
|
||||
/* show the image behind 'url' inline below 'ent', or hide it again if it is
|
||||
* already shown. Errors are reported to 'sess'. */
|
||||
void inline_image_toggle (session *sess, GtkXText *xtext, textentry *ent,
|
||||
const char *url);
|
||||
|
||||
/* replace image links in a message with a clickable placeholder label that
|
||||
* hides the URL. Returns a newly allocated string, or NULL if the text has
|
||||
* no image links (or the feature is off). */
|
||||
char *inline_image_filter_text (const char *text);
|
||||
|
||||
/* pop up a window with a larger version of the entry's inline image */
|
||||
void inline_image_show_viewer (GtkXText *xtext, textentry *ent);
|
||||
|
||||
/* can downloads go through the proxy configured for ZoiteChat (or is no
|
||||
* proxy configured)? FALSE means loading media would have to bypass the
|
||||
* user's proxy, which is never done. */
|
||||
gboolean inline_image_proxy_usable (void);
|
||||
|
||||
#endif
|
||||
@@ -137,7 +137,6 @@ joind_show_dialog (server *serv)
|
||||
gtk_window_set_position (GTK_WINDOW (dialog1), GTK_WIN_POS_CENTER_ON_PARENT);
|
||||
gtk_window_set_transient_for (GTK_WINDOW(dialog1), GTK_WINDOW(serv->front_session->gui->window));
|
||||
gtk_window_set_modal (GTK_WINDOW (dialog1), TRUE);
|
||||
gtk_window_set_resizable (GTK_WINDOW (dialog1), FALSE);
|
||||
|
||||
dialog_vbox1 = gtk_dialog_get_content_area (GTK_DIALOG (dialog1));
|
||||
gtk_widget_show (dialog_vbox1);
|
||||
|
||||
+292
-18
@@ -61,6 +61,7 @@
|
||||
#include "pixmaps.h"
|
||||
#include "plugin-tray.h"
|
||||
#include "xtext.h"
|
||||
#include "inline-image.h"
|
||||
#include "sexy-spell-entry.h"
|
||||
#include "gtkutil.h"
|
||||
|
||||
@@ -777,10 +778,13 @@ mg_inputbox_focus (GtkWidget *widget, GdkEventFocus *event, session_gui *gui)
|
||||
}
|
||||
|
||||
|
||||
#define TYPING_THROTTLE_USEC (3 * G_USEC_PER_SEC)
|
||||
|
||||
static gboolean
|
||||
mg_client_tag_allowed (server *serv, const char *tag)
|
||||
{
|
||||
char **deny;
|
||||
gboolean blocked = FALSE;
|
||||
int i;
|
||||
|
||||
if (!serv->have_message_tags)
|
||||
@@ -792,30 +796,42 @@ mg_client_tag_allowed (server *serv, const char *tag)
|
||||
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;
|
||||
}
|
||||
if (!strcmp (deny[i], "*"))
|
||||
blocked = TRUE;
|
||||
else if (deny[i][0] == '-' && !strcmp (deny[i] + 1, tag))
|
||||
blocked = FALSE;
|
||||
else if (!strcmp (deny[i], tag) || (deny[i][0] == '+' && !strcmp (deny[i] + 1, tag)))
|
||||
blocked = TRUE;
|
||||
}
|
||||
|
||||
g_strfreev (deny);
|
||||
return TRUE;
|
||||
return !blocked;
|
||||
}
|
||||
|
||||
static void
|
||||
static gboolean
|
||||
mg_typing_can_send (session *sess)
|
||||
{
|
||||
gint64 now = g_get_monotonic_time ();
|
||||
|
||||
return sess->typing_last_sent == 0 ||
|
||||
now - sess->typing_last_sent >= TYPING_THROTTLE_USEC;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
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;
|
||||
return FALSE;
|
||||
|
||||
if (sess->type != SESS_CHANNEL && sess->type != SESS_DIALOG)
|
||||
return;
|
||||
return FALSE;
|
||||
|
||||
g_snprintf (tags, sizeof (tags), "+typing=%s", state);
|
||||
sess->server->p_tagmsg (sess->server, tags, sess->channel);
|
||||
sess->typing_last_sent = g_get_monotonic_time ();
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static int
|
||||
@@ -844,17 +860,15 @@ mg_typing_update (session *sess, const char *text)
|
||||
|
||||
if (!text || !*text || text[0] == prefs.hex_input_command_char[0])
|
||||
{
|
||||
if (sess->typing_status)
|
||||
if (sess->typing_status && mg_typing_can_send (sess))
|
||||
mg_send_typing (sess, "done");
|
||||
sess->typing_status = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sess->typing_status != 1)
|
||||
{
|
||||
if (mg_typing_can_send (sess))
|
||||
mg_send_typing (sess, "active");
|
||||
sess->typing_status = 1;
|
||||
}
|
||||
sess->typing_status = 1;
|
||||
sess->typing_timeout_tag = fe_timeout_add_seconds (6, mg_typing_pause_cb, sess);
|
||||
}
|
||||
|
||||
@@ -926,10 +940,236 @@ mg_send_reply_or_text (session *sess, char *cmd)
|
||||
mg_reply_update (sess);
|
||||
}
|
||||
|
||||
typedef struct
|
||||
{
|
||||
gboolean bold;
|
||||
gboolean italic;
|
||||
gboolean underline;
|
||||
gboolean strikethrough;
|
||||
gboolean reverse;
|
||||
gint foreground;
|
||||
gint background;
|
||||
gboolean pending_foreground;
|
||||
gboolean pending_background;
|
||||
} MgInputFormatState;
|
||||
|
||||
static void
|
||||
mg_input_format_state_at_cursor (GtkEntry *entry, MgInputFormatState *state)
|
||||
{
|
||||
const char *text;
|
||||
const char *cursor;
|
||||
gsize limit;
|
||||
gsize i;
|
||||
gint cursor_pos;
|
||||
|
||||
memset (state, 0, sizeof (*state));
|
||||
state->foreground = -1;
|
||||
state->background = -1;
|
||||
|
||||
text = gtk_entry_get_text (entry);
|
||||
cursor_pos = gtk_editable_get_position (GTK_EDITABLE (entry));
|
||||
if (!text || cursor_pos < 0)
|
||||
return;
|
||||
|
||||
cursor = g_utf8_offset_to_pointer (text, cursor_pos);
|
||||
limit = (gsize) (cursor - text);
|
||||
|
||||
for (i = 0; i < limit;)
|
||||
{
|
||||
guchar ch = (guchar) text[i++];
|
||||
|
||||
switch (ch)
|
||||
{
|
||||
case ATTR_BOLD:
|
||||
state->bold = !state->bold;
|
||||
break;
|
||||
case ATTR_ITALICS:
|
||||
state->italic = !state->italic;
|
||||
break;
|
||||
case ATTR_UNDERLINE:
|
||||
state->underline = !state->underline;
|
||||
break;
|
||||
case ATTR_STRIKETHROUGH:
|
||||
state->strikethrough = !state->strikethrough;
|
||||
break;
|
||||
case ATTR_REVERSE:
|
||||
state->reverse = !state->reverse;
|
||||
break;
|
||||
case ATTR_RESET:
|
||||
case '\n':
|
||||
state->bold = FALSE;
|
||||
state->italic = FALSE;
|
||||
state->underline = FALSE;
|
||||
state->strikethrough = FALSE;
|
||||
state->reverse = FALSE;
|
||||
state->foreground = -1;
|
||||
state->background = -1;
|
||||
state->pending_foreground = FALSE;
|
||||
state->pending_background = FALSE;
|
||||
break;
|
||||
case ATTR_COLOR:
|
||||
{
|
||||
gint value = 0;
|
||||
gint digits = 0;
|
||||
|
||||
state->pending_foreground = FALSE;
|
||||
state->pending_background = FALSE;
|
||||
|
||||
if (i >= limit)
|
||||
{
|
||||
state->pending_foreground = TRUE;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!g_ascii_isdigit (text[i]))
|
||||
{
|
||||
state->foreground = -1;
|
||||
state->background = -1;
|
||||
break;
|
||||
}
|
||||
|
||||
while (i < limit && digits < 2 && g_ascii_isdigit (text[i]))
|
||||
{
|
||||
value = (value * 10) + (text[i] - '0');
|
||||
i++;
|
||||
digits++;
|
||||
}
|
||||
state->foreground = value;
|
||||
state->background = -1;
|
||||
|
||||
if (i < limit && text[i] == ',')
|
||||
{
|
||||
i++;
|
||||
if (i >= limit)
|
||||
{
|
||||
state->pending_background = TRUE;
|
||||
break;
|
||||
}
|
||||
|
||||
value = 0;
|
||||
digits = 0;
|
||||
while (i < limit && digits < 2 && g_ascii_isdigit (text[i]))
|
||||
{
|
||||
value = (value * 10) + (text[i] - '0');
|
||||
i++;
|
||||
digits++;
|
||||
}
|
||||
if (digits > 0)
|
||||
state->background = value;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
mg_input_format_append (GString *summary, const char *text)
|
||||
{
|
||||
if (summary->len > 0)
|
||||
g_string_append (summary, " · ");
|
||||
g_string_append (summary, text);
|
||||
}
|
||||
|
||||
static void
|
||||
mg_input_format_color_value (char *buffer, gsize buffer_len, const char *label, gint color_index)
|
||||
{
|
||||
guint16 red;
|
||||
guint16 green;
|
||||
guint16 blue;
|
||||
|
||||
if (theme_get_mirc_color_rgb16 ((unsigned int) color_index, &red, &green, &blue))
|
||||
{
|
||||
g_snprintf (buffer, buffer_len,
|
||||
"%s <b>%02d</b> <span foreground=\"#%02x%02x%02x\">■</span>",
|
||||
label, color_index, (guint) (red >> 8), (guint) (green >> 8),
|
||||
(guint) (blue >> 8));
|
||||
return;
|
||||
}
|
||||
|
||||
g_snprintf (buffer, buffer_len, "%s <b>%02d</b>", label, color_index);
|
||||
}
|
||||
|
||||
static void
|
||||
mg_input_format_update (GtkEntry *entry)
|
||||
{
|
||||
GtkWidget *format_box;
|
||||
GtkWidget *format_label;
|
||||
MgInputFormatState state;
|
||||
GString *summary;
|
||||
char color[96];
|
||||
char *markup;
|
||||
|
||||
format_box = g_object_get_data (G_OBJECT (entry), "zoitechat-format-box");
|
||||
format_label = g_object_get_data (G_OBJECT (entry), "zoitechat-format-label");
|
||||
if (!format_box || !format_label)
|
||||
return;
|
||||
|
||||
if (!prefs.hex_gui_input_attr)
|
||||
{
|
||||
gtk_widget_hide (format_box);
|
||||
return;
|
||||
}
|
||||
|
||||
mg_input_format_state_at_cursor (entry, &state);
|
||||
summary = g_string_new (NULL);
|
||||
|
||||
if (state.bold)
|
||||
mg_input_format_append (summary, "<b>Bold</b>");
|
||||
if (state.italic)
|
||||
mg_input_format_append (summary, "<i>Italic</i>");
|
||||
if (state.underline)
|
||||
mg_input_format_append (summary, "<u>Underline</u>");
|
||||
if (state.strikethrough)
|
||||
mg_input_format_append (summary, "<s>Strikethrough</s>");
|
||||
if (state.reverse)
|
||||
mg_input_format_append (summary, "Reverse");
|
||||
|
||||
if (state.foreground >= 0)
|
||||
{
|
||||
mg_input_format_color_value (color, sizeof (color), "Text color", state.foreground);
|
||||
mg_input_format_append (summary, color);
|
||||
}
|
||||
if (state.background >= 0)
|
||||
{
|
||||
mg_input_format_color_value (color, sizeof (color), "Background", state.background);
|
||||
mg_input_format_append (summary, color);
|
||||
}
|
||||
if (state.pending_foreground)
|
||||
mg_input_format_append (summary, "Color: type foreground[,background]");
|
||||
else if (state.pending_background)
|
||||
mg_input_format_append (summary, "Background: type color");
|
||||
|
||||
if (summary->len == 0)
|
||||
{
|
||||
gtk_widget_hide (format_box);
|
||||
g_string_free (summary, TRUE);
|
||||
return;
|
||||
}
|
||||
|
||||
markup = g_strdup_printf ("<span foreground='#7d8790'>Formatting · %s</span>", summary->str);
|
||||
gtk_label_set_markup (GTK_LABEL (format_label), markup);
|
||||
gtk_widget_show (format_label);
|
||||
gtk_widget_show (format_box);
|
||||
g_free (markup);
|
||||
g_string_free (summary, TRUE);
|
||||
}
|
||||
|
||||
static void
|
||||
mg_inputbox_cursor_changed (GObject *object, GParamSpec *pspec, gpointer userdata)
|
||||
{
|
||||
(void) pspec;
|
||||
(void) userdata;
|
||||
mg_input_format_update (GTK_ENTRY (object));
|
||||
}
|
||||
|
||||
static void
|
||||
mg_inputbox_changed (GtkEditable *editable, session_gui *gui)
|
||||
{
|
||||
key_check_replace_on_change (editable, NULL);
|
||||
mg_input_format_update (GTK_ENTRY (editable));
|
||||
if (current_sess && current_sess->gui == gui)
|
||||
mg_typing_update (current_sess, gtk_entry_get_text (GTK_ENTRY (editable)));
|
||||
}
|
||||
@@ -2090,7 +2330,6 @@ mg_open_quit_dialog (gboolean minimize_button)
|
||||
gtk_container_set_border_width (GTK_CONTAINER (dialog), 6);
|
||||
gtk_window_set_title (GTK_WINDOW (dialog), _("Quit ZoiteChat?"));
|
||||
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
|
||||
gtk_window_set_resizable (GTK_WINDOW (dialog), FALSE);
|
||||
|
||||
dialog_vbox1 = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
|
||||
gtk_widget_show (dialog_vbox1);
|
||||
@@ -3692,7 +3931,13 @@ mg_word_clicked (GtkWidget *xtext, char *word, GdkEventButton *even)
|
||||
{
|
||||
if (word == NULL)
|
||||
{
|
||||
mg_focus (sess);
|
||||
/* a click on a shown inline image opens the viewer */
|
||||
textentry *img_ent = gtk_xtext_get_clicked_image_entry (GTK_XTEXT (xtext));
|
||||
|
||||
if (img_ent)
|
||||
inline_image_show_viewer (GTK_XTEXT (xtext), img_ent);
|
||||
else
|
||||
mg_focus (sess);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3704,6 +3949,20 @@ mg_word_clicked (GtkWidget *xtext, char *word, GdkEventButton *even)
|
||||
case WORD_HOST6:
|
||||
case WORD_HOST:
|
||||
word[end] = 0;
|
||||
/* image links toggle an inline preview instead
|
||||
of opening the browser */
|
||||
if (word_type == WORD_URL &&
|
||||
prefs.hex_net_remote_media &&
|
||||
inline_image_is_image_url (word + start))
|
||||
{
|
||||
textentry *ent = gtk_xtext_get_clicked_entry (GTK_XTEXT (xtext));
|
||||
|
||||
if (ent)
|
||||
{
|
||||
inline_image_toggle (sess, GTK_XTEXT (xtext), ent, word + start);
|
||||
break;
|
||||
}
|
||||
}
|
||||
fe_open_url (word + start);
|
||||
}
|
||||
}
|
||||
@@ -3732,7 +3991,8 @@ mg_word_clicked (GtkWidget *xtext, char *word, GdkEventButton *even)
|
||||
case WORD_HOST:
|
||||
word[end] = 0;
|
||||
word += start;
|
||||
menu_urlmenu (even, word);
|
||||
menu_urlmenu_entry (even, word, xtext,
|
||||
gtk_xtext_get_clicked_entry (GTK_XTEXT (xtext)));
|
||||
break;
|
||||
case WORD_NICK:
|
||||
word[end] = 0;
|
||||
@@ -4679,9 +4939,19 @@ mg_create_search(session *sess, GtkWidget *box)
|
||||
static void
|
||||
mg_create_entry (session *sess, GtkWidget *box)
|
||||
{
|
||||
GtkWidget *hbox, *but, *entry;
|
||||
GtkWidget *hbox, *but, *entry, *format_box, *format_label;
|
||||
session_gui *gui = sess->gui;
|
||||
|
||||
format_box = mg_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6);
|
||||
gtk_widget_set_name (format_box, "zoitechat-formatbar");
|
||||
gtk_widget_set_no_show_all (format_box, TRUE);
|
||||
gtk_box_pack_start (GTK_BOX (box), format_box, 0, 0, 0);
|
||||
format_label = gtk_label_new ("");
|
||||
gtk_label_set_ellipsize (GTK_LABEL (format_label), PANGO_ELLIPSIZE_END);
|
||||
gtk_box_pack_start (GTK_BOX (format_box), format_label, TRUE, TRUE, 8);
|
||||
gtk_widget_show (format_label);
|
||||
gtk_widget_hide (format_box);
|
||||
|
||||
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);
|
||||
@@ -4712,12 +4982,16 @@ mg_create_entry (session *sess, GtkWidget *box)
|
||||
gui->input_box = entry = sexy_spell_entry_new ();
|
||||
sexy_spell_entry_set_checked ((SexySpellEntry *)entry, prefs.hex_gui_input_spell);
|
||||
sexy_spell_entry_set_parse_attributes ((SexySpellEntry *)entry, prefs.hex_gui_input_attr);
|
||||
g_object_set_data (G_OBJECT (entry), "zoitechat-format-box", format_box);
|
||||
g_object_set_data (G_OBJECT (entry), "zoitechat-format-label", format_label);
|
||||
|
||||
gtk_entry_set_max_length (GTK_ENTRY (gui->input_box), 0);
|
||||
g_signal_connect (G_OBJECT (entry), "activate",
|
||||
G_CALLBACK (mg_inputbox_cb), gui);
|
||||
g_signal_connect (G_OBJECT (entry), "changed",
|
||||
G_CALLBACK (mg_inputbox_changed), gui);
|
||||
g_signal_connect (G_OBJECT (entry), "notify::cursor-position",
|
||||
G_CALLBACK (mg_inputbox_cursor_changed), gui);
|
||||
gtk_box_pack_start (GTK_BOX (hbox), entry, TRUE, TRUE, 0);
|
||||
|
||||
gtk_widget_set_name (entry, "zoitechat-inputbox");
|
||||
|
||||
+61
-5
@@ -45,6 +45,7 @@
|
||||
#include "../common/util.h"
|
||||
#include "../common/text.h"
|
||||
#include "xtext.h"
|
||||
#include "inline-image.h"
|
||||
#include "ascii.h"
|
||||
#include "banlist.h"
|
||||
#include "chanlist.h"
|
||||
@@ -1056,8 +1057,40 @@ open_url_cb (GtkWidget *item, char *url)
|
||||
handle_command (current_sess, buf, FALSE);
|
||||
}
|
||||
|
||||
/* the text entry the open URL menu was created for; the widget pointer is
|
||||
nulled automatically if the widget dies while the menu is up */
|
||||
static GtkXText *urlmenu_xtext = NULL;
|
||||
static textentry *urlmenu_ent = NULL;
|
||||
|
||||
static void
|
||||
menu_urlmenu_set_entry (GtkWidget *xtext, textentry *ent)
|
||||
{
|
||||
if (urlmenu_xtext)
|
||||
g_object_remove_weak_pointer (G_OBJECT (urlmenu_xtext),
|
||||
(gpointer *) &urlmenu_xtext);
|
||||
urlmenu_xtext = xtext ? GTK_XTEXT (xtext) : NULL;
|
||||
urlmenu_ent = ent;
|
||||
if (urlmenu_xtext)
|
||||
g_object_add_weak_pointer (G_OBJECT (urlmenu_xtext),
|
||||
(gpointer *) &urlmenu_xtext);
|
||||
}
|
||||
|
||||
static void
|
||||
inline_image_menu_cb (GtkWidget *item, char *url)
|
||||
{
|
||||
if (urlmenu_xtext == NULL || urlmenu_ent == NULL)
|
||||
return;
|
||||
|
||||
/* the line may have scrolled out of the buffer since the menu opened */
|
||||
if (!gtk_xtext_buffer_contains (urlmenu_xtext, urlmenu_ent))
|
||||
return;
|
||||
|
||||
inline_image_toggle (current_sess, urlmenu_xtext, urlmenu_ent, url);
|
||||
}
|
||||
|
||||
void
|
||||
menu_urlmenu (GdkEventButton *event, char *url)
|
||||
menu_urlmenu_entry (GdkEventButton *event, char *url, GtkWidget *xtext,
|
||||
textentry *ent)
|
||||
{
|
||||
GtkWidget *menu;
|
||||
char *tmp, *chop;
|
||||
@@ -1065,6 +1098,14 @@ menu_urlmenu (GdkEventButton *event, char *url)
|
||||
g_free (str_copy);
|
||||
str_copy = g_strdup (url);
|
||||
|
||||
if (xtext == NULL || ent == NULL || !prefs.hex_net_remote_media ||
|
||||
!inline_image_is_image_url (str_copy))
|
||||
{
|
||||
xtext = NULL;
|
||||
ent = NULL;
|
||||
}
|
||||
menu_urlmenu_set_entry (xtext, ent);
|
||||
|
||||
menu = menu_new ();
|
||||
/* more than 51 chars? Chop it */
|
||||
if (g_utf8_strlen (str_copy, -1) >= 52)
|
||||
@@ -1087,6 +1128,15 @@ menu_urlmenu (GdkEventButton *event, char *url)
|
||||
menu_quick_item_with_callback (open_url_cb, _("Connect"), menu, str_copy);
|
||||
else
|
||||
menu_quick_item_with_callback (open_url_cb, _("Open Link in Browser"), menu, str_copy);
|
||||
if (ent)
|
||||
{
|
||||
const char *shown = gtk_xtext_entry_get_image_url (GTK_XTEXT (xtext), ent);
|
||||
|
||||
menu_quick_item_with_callback (inline_image_menu_cb,
|
||||
shown && strcmp (shown, str_copy) == 0 ?
|
||||
_("Hide Inline Image") : _("Show Image Inline"),
|
||||
menu, str_copy);
|
||||
}
|
||||
menu_quick_item_with_callback (copy_to_clipboard_cb, _("Copy Selected Link"), menu, str_copy);
|
||||
/* custom ones from urlhandlers.conf */
|
||||
menu_create (menu, urlhandler_list, str_copy, TRUE);
|
||||
@@ -1094,6 +1144,12 @@ menu_urlmenu (GdkEventButton *event, char *url)
|
||||
menu_popup (menu, event, NULL);
|
||||
}
|
||||
|
||||
void
|
||||
menu_urlmenu (GdkEventButton *event, char *url)
|
||||
{
|
||||
menu_urlmenu_entry (event, url, NULL, NULL);
|
||||
}
|
||||
|
||||
static void
|
||||
menu_chan_cycle (GtkWidget * menu, char *chan)
|
||||
{
|
||||
@@ -1539,16 +1595,16 @@ menu_join (GtkWidget * wid, gpointer none)
|
||||
NULL);
|
||||
theme_manager_attach_window (dialog);
|
||||
content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
|
||||
gtk_box_set_homogeneous (GTK_BOX (content_area), TRUE);
|
||||
gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_MOUSE);
|
||||
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, TRUE, 0);
|
||||
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6);
|
||||
|
||||
entry = gtk_entry_new ();
|
||||
gtk_editable_set_editable (GTK_EDITABLE (entry), FALSE); /* avoid auto-selection */
|
||||
gtk_entry_set_text (GTK_ENTRY (entry), "#");
|
||||
g_signal_connect (G_OBJECT (entry), "activate",
|
||||
G_CALLBACK (menu_join_entry_cb), dialog);
|
||||
gtk_box_pack_end (GTK_BOX (hbox), entry, 0, 0, 0);
|
||||
gtk_widget_set_valign (entry, GTK_ALIGN_CENTER);
|
||||
gtk_box_pack_end (GTK_BOX (hbox), entry, TRUE, TRUE, 0);
|
||||
|
||||
label = gtk_label_new (_("Enter Channel to Join:"));
|
||||
gtk_box_pack_end (GTK_BOX (hbox), label, 0, 0, 0);
|
||||
@@ -1556,7 +1612,7 @@ menu_join (GtkWidget * wid, gpointer none)
|
||||
g_signal_connect (G_OBJECT (dialog), "response",
|
||||
G_CALLBACK (menu_join_cb), entry);
|
||||
|
||||
gtk_container_add (GTK_CONTAINER (content_area), hbox);
|
||||
gtk_box_pack_start (GTK_BOX (content_area), hbox, TRUE, TRUE, 0);
|
||||
|
||||
gtk_widget_show_all (dialog);
|
||||
|
||||
|
||||
@@ -20,8 +20,11 @@
|
||||
#ifndef ZOITECHAT_MENU_H
|
||||
#define ZOITECHAT_MENU_H
|
||||
|
||||
#include "xtext.h"
|
||||
|
||||
GtkWidget *menu_create_main (void *accel_group, int bar, int away, int toplevel, GtkWidget **menu_widgets);
|
||||
void menu_urlmenu (GdkEventButton * event, char *url);
|
||||
void menu_urlmenu_entry (GdkEventButton *event, char *url, GtkWidget *xtext, textentry *ent);
|
||||
void menu_chanmenu (session *sess, GdkEventButton * event, char *chan);
|
||||
void menu_addfavoritemenu (server *serv, GtkWidget *menu, char *channel, gboolean istree);
|
||||
void menu_addconnectmenu (server *serv, GtkWidget *menu);
|
||||
|
||||
@@ -25,6 +25,7 @@ zoitechat_gtk_sources = [
|
||||
'gtkutil.c',
|
||||
'icon-resolver.c',
|
||||
'ignoregui.c',
|
||||
'inline-image.c',
|
||||
'joind.c',
|
||||
'menu.c',
|
||||
'maingui.c',
|
||||
@@ -110,7 +111,7 @@ zoitechat_theme_lib = static_library('zoitechat_theme',
|
||||
iso_codes = dependency('iso-codes', required: false)
|
||||
if iso_codes.found()
|
||||
zoitechat_gtk_sources += 'sexy-iso-codes.c'
|
||||
iso_codes_prefix = iso_codes.get_pkgconfig_variable('prefix')
|
||||
iso_codes_prefix = iso_codes.get_variable(pkgconfig : 'prefix')
|
||||
zoitechat_gtk_cflags += '-DISO_CODES_PREFIX="@0@"'.format(iso_codes_prefix)
|
||||
zoitechat_gtk_cflags += '-DISO_CODES_LOCALEDIR="@0@"'.format(
|
||||
join_paths(iso_codes_prefix, 'share/locale'))
|
||||
@@ -139,7 +140,7 @@ executable('zoitechat',
|
||||
link_args: zoitechat_gtk_ldflags,
|
||||
pie: true,
|
||||
install: true,
|
||||
gui_app: true,
|
||||
win_subsystem: 'windows',
|
||||
)
|
||||
|
||||
emoji_ui_tests = executable('emoji_ui_tests',
|
||||
|
||||
+11
-6
@@ -391,34 +391,39 @@ fe_notify_ask (char *nick, char *networks)
|
||||
gtk_container_set_border_width (GTK_CONTAINER (table), 12);
|
||||
gtk_grid_set_row_spacing (GTK_GRID (table), 3);
|
||||
gtk_grid_set_column_spacing (GTK_GRID (table), 8);
|
||||
gtk_container_add (GTK_CONTAINER (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), table);
|
||||
gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))),
|
||||
table, TRUE, TRUE, 0);
|
||||
|
||||
label = gtk_label_new (msg);
|
||||
gtkutil_grid_attach_defaults (table, label, 0, 1, 0, 1);
|
||||
gtk_label_set_xalign (GTK_LABEL (label), 0.0f);
|
||||
gtkutil_grid_attach (table, label, 0, 1, 0, 1, GTKUTIL_ATTACH_FILL, 0, 0, 0);
|
||||
|
||||
entry = gtk_entry_new ();
|
||||
gtk_entry_set_text (GTK_ENTRY (entry), nick);
|
||||
g_signal_connect (G_OBJECT (entry), "activate",
|
||||
G_CALLBACK (notifygui_add_enter), dialog);
|
||||
gtkutil_grid_attach_defaults (table, entry, 1, 2, 0, 1);
|
||||
gtkutil_grid_attach (table, entry, 1, 2, 0, 1,
|
||||
GTKUTIL_ATTACH_EXPAND | GTKUTIL_ATTACH_FILL, 0, 0, 0);
|
||||
|
||||
g_signal_connect (G_OBJECT (dialog), "response",
|
||||
G_CALLBACK (notifygui_add_cb), entry);
|
||||
|
||||
label = gtk_label_new (_("Notify on these networks:"));
|
||||
gtkutil_grid_attach_defaults (table, label, 0, 1, 2, 3);
|
||||
gtk_label_set_xalign (GTK_LABEL (label), 0.0f);
|
||||
gtkutil_grid_attach (table, label, 0, 1, 2, 3, GTKUTIL_ATTACH_FILL, 0, 0, 0);
|
||||
|
||||
wid = gtk_entry_new ();
|
||||
g_object_set_data (G_OBJECT (entry), "net", wid);
|
||||
g_signal_connect (G_OBJECT (wid), "activate",
|
||||
G_CALLBACK (notifygui_add_enter), dialog);
|
||||
gtk_entry_set_text (GTK_ENTRY (wid), networks ? networks : "ALL");
|
||||
gtkutil_grid_attach_defaults (table, wid, 1, 2, 2, 3);
|
||||
gtkutil_grid_attach (table, wid, 1, 2, 2, 3,
|
||||
GTKUTIL_ATTACH_EXPAND | GTKUTIL_ATTACH_FILL, 0, 0, 0);
|
||||
|
||||
label = gtk_label_new (NULL);
|
||||
g_snprintf (buf, sizeof (buf), "<i><span size=\"smaller\">%s</span></i>", _("Comma separated list of networks is accepted."));
|
||||
gtk_label_set_markup (GTK_LABEL (label), buf);
|
||||
gtkutil_grid_attach_defaults (table, label, 1, 2, 3, 4);
|
||||
gtkutil_grid_attach (table, label, 1, 2, 3, 4, GTKUTIL_ATTACH_FILL, 0, 0, 0);
|
||||
|
||||
gtk_widget_show_all (dialog);
|
||||
}
|
||||
|
||||
@@ -2423,6 +2423,8 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
|
||||
GtkWidget *comboboxentry_charset;
|
||||
GtkWidget *combobox_logintypes;
|
||||
GtkWidget *hbox1;
|
||||
GtkWidget *content_scroll;
|
||||
GtkWidget *content_box;
|
||||
GtkWidget *scrolledwindow2;
|
||||
GtkWidget *scrolledwindow4;
|
||||
GtkWidget *scrolledwindow5;
|
||||
@@ -2449,7 +2451,9 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
|
||||
gtk_container_set_border_width (GTK_CONTAINER (editwindow), 4);
|
||||
g_snprintf (buf, sizeof (buf), _("Edit %s - %s"), net->name, _(DISPLAY_NAME));
|
||||
gtk_window_set_title (GTK_WINDOW (editwindow), buf);
|
||||
gtk_window_set_default_size (GTK_WINDOW (editwindow), netedit_win_width, netedit_win_height);
|
||||
gtk_window_set_default_size (GTK_WINDOW (editwindow),
|
||||
netedit_win_width > 0 ? netedit_win_width : -1,
|
||||
netedit_win_height > 0 ? netedit_win_height : -1);
|
||||
gtk_window_set_transient_for (GTK_WINDOW (editwindow), GTK_WINDOW (parent));
|
||||
gtk_window_set_modal (GTK_WINDOW (editwindow), TRUE);
|
||||
gtk_window_set_type_hint (GTK_WINDOW (editwindow), GDK_WINDOW_TYPE_HINT_DIALOG);
|
||||
@@ -2458,10 +2462,20 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
|
||||
vbox5 = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 0);
|
||||
gtk_container_add (GTK_CONTAINER (editwindow), vbox5);
|
||||
|
||||
/* Keep Close accessible on small screens, while the lists still receive
|
||||
all spare space when the complete form fits in the window. */
|
||||
content_scroll = gtk_scrolled_window_new (NULL, NULL);
|
||||
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (content_scroll),
|
||||
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
|
||||
gtk_scrolled_window_set_propagate_natural_width (GTK_SCROLLED_WINDOW (content_scroll), TRUE);
|
||||
gtk_scrolled_window_set_propagate_natural_height (GTK_SCROLLED_WINDOW (content_scroll), TRUE);
|
||||
gtk_box_pack_start (GTK_BOX (vbox5), content_scroll, TRUE, TRUE, 0);
|
||||
content_box = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 0);
|
||||
gtk_container_add (GTK_CONTAINER (content_scroll), content_box);
|
||||
|
||||
/* Tabs and buttons */
|
||||
hbox1 = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 0);
|
||||
gtk_box_pack_start (GTK_BOX (vbox5), hbox1, TRUE, TRUE, 4);
|
||||
gtk_box_pack_start (GTK_BOX (content_box), hbox1, TRUE, TRUE, 4);
|
||||
|
||||
scrolledwindow2 = gtk_scrolled_window_new (NULL, NULL);
|
||||
scrolledwindow4 = gtk_scrolled_window_new (NULL, NULL);
|
||||
@@ -2599,7 +2613,7 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
|
||||
|
||||
/* Checkboxes and entries */
|
||||
table3 = gtkutil_grid_new (19, 2, FALSE);
|
||||
gtk_box_pack_start (GTK_BOX (vbox5), table3, FALSE, FALSE, 0);
|
||||
gtk_box_pack_start (GTK_BOX (content_box), table3, FALSE, FALSE, 0);
|
||||
gtk_grid_set_row_spacing (GTK_GRID (table3), 2);
|
||||
gtk_grid_set_column_spacing (GTK_GRID (table3), 8);
|
||||
|
||||
@@ -2828,7 +2842,9 @@ servlist_open_networks (void)
|
||||
gtk_container_set_border_width (GTK_CONTAINER (servlist), 4);
|
||||
g_snprintf(buf, sizeof(buf), _("Network List - %s"), _(DISPLAY_NAME));
|
||||
gtk_window_set_title (GTK_WINDOW (servlist), buf);
|
||||
gtk_window_set_default_size (GTK_WINDOW (servlist), netlist_win_width, netlist_win_height);
|
||||
gtk_window_set_default_size (GTK_WINDOW (servlist),
|
||||
netlist_win_width > 0 ? netlist_win_width : -1,
|
||||
netlist_win_height > 0 ? netlist_win_height : -1);
|
||||
gtk_window_set_role (GTK_WINDOW (servlist), "servlist");
|
||||
gtk_window_set_type_hint (GTK_WINDOW (servlist), GDK_WINDOW_TYPE_HINT_DIALOG);
|
||||
if (current_sess)
|
||||
|
||||
+25
-8
@@ -340,6 +340,9 @@ static const setting tabs_settings[] =
|
||||
{ST_TOGGLE, N_("Show close button on tabs"), P_OFFINTNL(hex_gui_tab_closebuttons), 0, 0, 0},
|
||||
{ST_TOGGLE, N_("Middle click to close tab"), P_OFFINTNL(hex_gui_tab_middleclose), 0, 0, 0},
|
||||
{ST_TOGGLE, N_("Smaller text"), P_OFFINTNL(hex_gui_tab_small), 0, 0, 0},
|
||||
{ST_EFONT, N_("Tab font:"), P_OFFSETNL(hex_gui_tab_font),
|
||||
N_("Font for server and channel tabs in the Tabs layout. Leave blank to use the theme font. Smaller text still applies."),
|
||||
0, sizeof prefs.hex_gui_tab_font},
|
||||
{ST_MENU, N_("Focus new tabs:"), P_OFFINTNL(hex_gui_tab_newtofront), 0, focusnewtabsmenu, 0},
|
||||
{ST_MENU, N_("Placement of notices:"), P_OFFINTNL(hex_irc_notice_pos), 0, noticeposmenu, 0},
|
||||
{ST_MENU, N_("Show channel switcher at:"), P_OFFINTNL(hex_gui_tab_pos), 0, cspos, 1},
|
||||
@@ -674,6 +677,11 @@ static const setting network_settings[] =
|
||||
{ST_ENTRY, N_("Username:"), P_OFFSETNL(hex_net_proxy_user), 0, 0, sizeof prefs.hex_net_proxy_user},
|
||||
{ST_ENTRY, N_("Password:"), P_OFFSETNL(hex_net_proxy_pass), 0, GINT_TO_POINTER(1), sizeof prefs.hex_net_proxy_pass},
|
||||
|
||||
{ST_HEADER, N_("Remote Media"), 0, 0, 0, 0},
|
||||
{ST_TOGGLE, N_("Allow loading remote media (inline images) when clicked"),
|
||||
P_OFFINTNL(hex_net_remote_media),
|
||||
N_("Loading remote media contacts the site hosting it, which can reveal your IP address and that you viewed the message. You are asked for confirmation before each new domain is contacted."), 0, 0},
|
||||
|
||||
{ST_END, 0, 0, 0, 0, 0}
|
||||
};
|
||||
|
||||
@@ -1248,7 +1256,7 @@ setup_create_id_menu (GtkWidget *table, char *label, int row, char *dest)
|
||||
static void
|
||||
setup_create_menu (GtkWidget *table, int row, const setting *set)
|
||||
{
|
||||
GtkWidget *wid, *cbox, *box;
|
||||
GtkWidget *wid, *cbox;
|
||||
const char **text = (const char **)set->list;
|
||||
int i;
|
||||
|
||||
@@ -1269,9 +1277,7 @@ setup_create_menu (GtkWidget *table, int row, const setting *set)
|
||||
g_signal_connect (G_OBJECT (cbox), "changed",
|
||||
G_CALLBACK (setup_menu_cb), (gpointer)set);
|
||||
|
||||
box = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 0);
|
||||
gtk_box_pack_start (GTK_BOX (box), cbox, 0, 0, 0);
|
||||
setup_table_attach (table, box, 3, 4, row, row + 1, TRUE, FALSE,
|
||||
setup_table_attach (table, cbox, 3, 6, row, row + 1, TRUE, FALSE,
|
||||
SETUP_ALIGN_FILL, SETUP_ALIGN_FILL, 0, 0);
|
||||
}
|
||||
|
||||
@@ -1955,7 +1961,8 @@ setup_add_page (const char *title, GtkWidget *book, GtkWidget *tab)
|
||||
|
||||
sw = GTK_SCROLLED_WINDOW(gtk_scrolled_window_new (NULL, NULL));
|
||||
gtk_scrolled_window_set_shadow_type (sw, GTK_SHADOW_IN);
|
||||
gtk_scrolled_window_set_policy (sw, GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||
gtk_scrolled_window_set_policy (sw, GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
|
||||
gtk_scrolled_window_set_propagate_natural_width (sw, TRUE);
|
||||
gtk_container_add (GTK_CONTAINER (sw), vvbox);
|
||||
|
||||
viewport = gtk_bin_get_child (GTK_BIN(sw));
|
||||
@@ -1999,7 +2006,9 @@ setup_ensure_page_created (int page)
|
||||
}
|
||||
|
||||
page_widget = setup_page_factories[page] (setup_page_settings[page]);
|
||||
gtk_container_add (GTK_CONTAINER (container), page_widget);
|
||||
/* Lazy pages must pass the notebook's spare space to their content,
|
||||
particularly the independently scrolling Sounds list. */
|
||||
gtk_box_pack_start (GTK_BOX (container), page_widget, TRUE, TRUE, 0);
|
||||
gtk_widget_show_all (container);
|
||||
}
|
||||
|
||||
@@ -2177,9 +2186,14 @@ setup_create_tree (GtkWidget *box, GtkWidget *book)
|
||||
-1, _("Categories"), renderer, "text", 0, NULL);
|
||||
gtk_tree_view_expand_all (GTK_TREE_VIEW (tree));
|
||||
|
||||
frame = gtk_frame_new (NULL);
|
||||
frame = gtk_scrolled_window_new (NULL, NULL);
|
||||
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (frame),
|
||||
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||
gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (frame), GTK_SHADOW_IN);
|
||||
gtk_scrolled_window_set_propagate_natural_width (GTK_SCROLLED_WINDOW (frame), TRUE);
|
||||
gtk_widget_set_vexpand (frame, TRUE);
|
||||
gtk_container_add (GTK_CONTAINER (frame), tree);
|
||||
gtk_box_pack_start (GTK_BOX (box), frame, 0, 0, 0);
|
||||
gtk_box_pack_start (GTK_BOX (box), frame, FALSE, TRUE, 0);
|
||||
gtk_box_reorder_child (GTK_BOX (box), frame, 0);
|
||||
|
||||
if (sel_iter)
|
||||
@@ -2330,6 +2344,9 @@ setup_apply (struct zoitechatprefs *pr)
|
||||
if (DIFF (hex_gui_tab_icons) || DIFF (hex_gui_tab_closebuttons) ||
|
||||
DIFF (hex_gui_tab_small))
|
||||
live_changes.chanview = TRUE;
|
||||
if (pr->hex_gui_tab_layout == 0 &&
|
||||
strcmp (pr->hex_gui_tab_font, prefs.hex_gui_tab_font) != 0)
|
||||
live_changes.chanview = TRUE;
|
||||
if (DIFF (hex_gui_tab_sort))
|
||||
live_changes.tab_resort = TRUE;
|
||||
if (DIFF (hex_gui_tab_trunc))
|
||||
|
||||
@@ -1266,8 +1266,6 @@ attr_list_has_attrs (PangoAttrList *attrs)
|
||||
static void
|
||||
sexy_spell_entry_recheck_all(SexySpellEntry *entry)
|
||||
{
|
||||
GdkRectangle rect;
|
||||
GtkAllocation allocation;
|
||||
GtkWidget *widget = GTK_WIDGET(entry);
|
||||
int length, i, text_len;
|
||||
const char *text;
|
||||
@@ -1299,15 +1297,8 @@ sexy_spell_entry_recheck_all(SexySpellEntry *entry)
|
||||
|
||||
gtk_entry_set_attributes (GTK_ENTRY (entry), attr_list_has_attrs (entry->priv->attr_list) ? entry->priv->attr_list : NULL);
|
||||
|
||||
if (gtk_widget_get_realized (GTK_WIDGET(entry)))
|
||||
{
|
||||
gtk_widget_get_allocation (GTK_WIDGET(entry), &allocation);
|
||||
|
||||
rect.x = 0; rect.y = 0;
|
||||
rect.width = allocation.width;
|
||||
rect.height = allocation.height;
|
||||
gdk_window_invalidate_rect(gtk_widget_get_window (widget), &rect, TRUE);
|
||||
}
|
||||
/* GtkEntry shares its parent's GdkWindow in GTK3; redraw the widget area. */
|
||||
gtk_widget_queue_draw (widget);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
|
||||
+10
-7
@@ -400,7 +400,6 @@ pevent_treeview_new (GtkWidget *box)
|
||||
scroll = gtk_scrolled_window_new (NULL, NULL);
|
||||
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
|
||||
gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scroll), GTK_SHADOW_IN);
|
||||
gtk_widget_set_size_request (GTK_WIDGET (scroll), -1, 250);
|
||||
|
||||
store = gtk_list_store_new (N_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_INT);
|
||||
g_return_val_if_fail (store != NULL, NULL);
|
||||
@@ -437,7 +436,7 @@ pevent_treeview_new (GtkWidget *box)
|
||||
gtk_tree_view_column_set_min_width (col, 100);
|
||||
|
||||
gtk_container_add (GTK_CONTAINER (scroll), view);
|
||||
gtk_container_add (GTK_CONTAINER (box), scroll);
|
||||
gtk_paned_pack1 (GTK_PANED (box), scroll, TRUE, FALSE);
|
||||
|
||||
return view;
|
||||
}
|
||||
@@ -481,7 +480,7 @@ pevent_hlist_treeview_new (GtkWidget *box)
|
||||
gtk_tree_view_column_set_sizing (col, GTK_TREE_VIEW_COLUMN_AUTOSIZE);
|
||||
|
||||
gtk_container_add (GTK_CONTAINER (scroll), view);
|
||||
gtk_container_add (GTK_CONTAINER (box), scroll);
|
||||
gtk_paned_pack2 (GTK_PANED (box), scroll, TRUE, FALSE);
|
||||
|
||||
return view;
|
||||
}
|
||||
@@ -489,7 +488,7 @@ pevent_hlist_treeview_new (GtkWidget *box)
|
||||
void
|
||||
pevent_dialog_show ()
|
||||
{
|
||||
GtkWidget *vbox, *hbox, *wid, *pane;
|
||||
GtkWidget *vbox, *hbox, *wid, *pane, *preview_pane;
|
||||
XTextColor xtext_palette[XTEXT_COLS];
|
||||
|
||||
if (pevent_dialog)
|
||||
@@ -503,8 +502,12 @@ pevent_dialog_show ()
|
||||
TRUE, FALSE, pevent_dialog_close, NULL,
|
||||
600, 455, &vbox, 0);
|
||||
|
||||
preview_pane = gtk_paned_new (GTK_ORIENTATION_VERTICAL);
|
||||
gtk_box_pack_start (GTK_BOX (vbox), preview_pane, TRUE, TRUE, 0);
|
||||
pane = gtk_paned_new (GTK_ORIENTATION_VERTICAL);
|
||||
gtk_box_pack_start (GTK_BOX (vbox), pane, TRUE, TRUE, 0);
|
||||
gtk_paned_pack1 (GTK_PANED (preview_pane), pane, TRUE, FALSE);
|
||||
gtk_paned_set_position (GTK_PANED (pane), 220);
|
||||
gtk_paned_set_position (GTK_PANED (preview_pane), 330);
|
||||
|
||||
pevent_dialog_list = pevent_treeview_new (pane);
|
||||
pevent_dialog_fill (pevent_dialog_list);
|
||||
@@ -513,12 +516,12 @@ pevent_dialog_show ()
|
||||
|
||||
wid = gtk_scrolled_window_new (NULL, NULL);
|
||||
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (wid), GTK_POLICY_NEVER, GTK_POLICY_ALWAYS);
|
||||
gtk_box_pack_start (GTK_BOX (vbox), wid, FALSE, TRUE, 0);
|
||||
gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (wid), 75);
|
||||
gtk_paned_pack2 (GTK_PANED (preview_pane), wid, TRUE, FALSE);
|
||||
|
||||
theme_get_xtext_colors_for_widget (wid, xtext_palette, XTEXT_COLS);
|
||||
pevent_dialog_twid = gtk_xtext_new (xtext_palette, 0);
|
||||
gtk_widget_set_sensitive (pevent_dialog_twid, FALSE);
|
||||
gtk_widget_set_size_request (pevent_dialog_twid, -1, 75);
|
||||
gtk_container_add (GTK_CONTAINER (wid), pevent_dialog_twid);
|
||||
gtk_xtext_set_font (GTK_XTEXT (pevent_dialog_twid), prefs.hex_text_font);
|
||||
g_object_set_data (G_OBJECT (pevent_dialog), "xtext", pevent_dialog_twid);
|
||||
|
||||
@@ -683,7 +683,7 @@ theme_manager_get_userlist_palette_behavior (const PangoFontDescription *font_de
|
||||
|
||||
behavior.font_desc = font_desc;
|
||||
behavior.apply_background = TRUE;
|
||||
behavior.apply_foreground = (prefs.hex_gui_ulist_color || prefs.hex_text_color_nicks) ? FALSE : TRUE;
|
||||
behavior.apply_foreground = prefs.hex_gui_ulist_color ? FALSE : TRUE;
|
||||
|
||||
return behavior;
|
||||
}
|
||||
|
||||
@@ -1911,6 +1911,7 @@ theme_preferences_create_page (GtkWindow *parent,
|
||||
G_TYPE_STRING,
|
||||
G_TYPE_INT);
|
||||
ui->gtk3_combo = gtk_combo_box_new_with_model (GTK_TREE_MODEL (gtk3_store));
|
||||
gtk_widget_set_hexpand (ui->gtk3_combo, TRUE);
|
||||
g_object_unref (gtk3_store);
|
||||
renderer = gtk_cell_renderer_text_new ();
|
||||
gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (ui->gtk3_combo), renderer, TRUE);
|
||||
|
||||
@@ -594,7 +594,7 @@ fe_userlist_rehash (session *sess, struct User *user)
|
||||
nick_token = THEME_TOKEN_TAB_AWAY;
|
||||
have_nick_token = TRUE;
|
||||
}
|
||||
else if (prefs.hex_gui_ulist_color || prefs.hex_text_color_nicks)
|
||||
else if (prefs.hex_gui_ulist_color)
|
||||
{
|
||||
int mirc_index = text_color_of (user->nick);
|
||||
|
||||
@@ -635,7 +635,7 @@ fe_userlist_insert (session *sess, struct User *newuser, gboolean sel)
|
||||
nick_token = THEME_TOKEN_TAB_AWAY;
|
||||
have_nick_token = TRUE;
|
||||
}
|
||||
else if (prefs.hex_gui_ulist_color || prefs.hex_text_color_nicks)
|
||||
else if (prefs.hex_gui_ulist_color)
|
||||
{
|
||||
int mirc_index = text_color_of (newuser->nick);
|
||||
|
||||
|
||||
+599
-20
@@ -93,8 +93,104 @@ struct textentry
|
||||
guchar pad1;
|
||||
guchar pad2;
|
||||
GList *marks; /* List of found strings */
|
||||
GdkPixbuf *image; /* inline image shown below the text, or NULL */
|
||||
GdkPixbuf *image_full; /* larger version for the image viewer */
|
||||
char *image_url; /* the URL the image was loaded from */
|
||||
};
|
||||
|
||||
/* inline image display */
|
||||
#define XTEXT_IMAGE_PADDING 4
|
||||
#define XTEXT_IMAGE_MAX_WIDTH 480
|
||||
#define XTEXT_IMAGE_MAX_HEIGHT 360
|
||||
#define XTEXT_IMAGE_FULL_MAX_WIDTH 1600
|
||||
#define XTEXT_IMAGE_FULL_MAX_HEIGHT 1200
|
||||
|
||||
/* an inline image load that has been started but not yet completed;
|
||||
ent/buf/xtext are invalidated (entry removed) when any of them dies */
|
||||
typedef struct
|
||||
{
|
||||
guint id;
|
||||
GtkXText *xtext;
|
||||
xtext_buffer *buf;
|
||||
textentry *ent;
|
||||
char *url;
|
||||
} xtext_image_pending;
|
||||
|
||||
static GSList *image_pending_list = NULL;
|
||||
static guint image_pending_next_id = 1;
|
||||
|
||||
static void
|
||||
gtk_xtext_image_pending_forget (gboolean (*match) (xtext_image_pending *, gconstpointer), gconstpointer data)
|
||||
{
|
||||
GSList *list = image_pending_list;
|
||||
|
||||
while (list)
|
||||
{
|
||||
GSList *next = list->next;
|
||||
xtext_image_pending *pending = list->data;
|
||||
|
||||
if (match (pending, data))
|
||||
{
|
||||
image_pending_list = g_slist_delete_link (image_pending_list, list);
|
||||
g_free (pending->url);
|
||||
g_free (pending);
|
||||
}
|
||||
list = next;
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean
|
||||
gtk_xtext_image_pending_match_ent (xtext_image_pending *pending, gconstpointer ent)
|
||||
{
|
||||
return pending->ent == ent;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
gtk_xtext_image_pending_match_buf (xtext_image_pending *pending, gconstpointer buf)
|
||||
{
|
||||
return pending->buf == buf;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
gtk_xtext_image_pending_match_xtext (xtext_image_pending *pending, gconstpointer xtext)
|
||||
{
|
||||
return pending->xtext == xtext;
|
||||
}
|
||||
|
||||
/* number of extra screen lines an entry needs for its inline image */
|
||||
|
||||
static int
|
||||
gtk_xtext_image_rows (GtkXText *xtext, textentry *ent)
|
||||
{
|
||||
if (ent->image == NULL || xtext->fontsize <= 0)
|
||||
return 0;
|
||||
|
||||
return (gdk_pixbuf_get_height (ent->image) + XTEXT_IMAGE_PADDING * 2 +
|
||||
xtext->fontsize - 1) / xtext->fontsize;
|
||||
}
|
||||
|
||||
/* free an entry's inline image and drop any pending load aimed at it;
|
||||
must be called whenever a textentry is freed */
|
||||
|
||||
static void
|
||||
gtk_xtext_ent_free_image (textentry *ent)
|
||||
{
|
||||
gtk_xtext_image_pending_forget (gtk_xtext_image_pending_match_ent, ent);
|
||||
|
||||
if (ent->image)
|
||||
{
|
||||
g_object_unref (ent->image);
|
||||
ent->image = NULL;
|
||||
}
|
||||
if (ent->image_full)
|
||||
{
|
||||
g_object_unref (ent->image_full);
|
||||
ent->image_full = NULL;
|
||||
}
|
||||
g_free (ent->image_url);
|
||||
ent->image_url = NULL;
|
||||
}
|
||||
|
||||
enum
|
||||
{
|
||||
WORD_CLICK,
|
||||
@@ -177,6 +273,8 @@ static void gtk_xtext_render_page (GtkXText * xtext);
|
||||
static void gtk_xtext_calc_lines (xtext_buffer *buf, int);
|
||||
static gboolean gtk_xtext_is_selecting (GtkXText *xtext);
|
||||
static char *gtk_xtext_selection_get_text (GtkXText *xtext, int *len_ret);
|
||||
static void gtk_xtext_clipboard_get (GtkClipboard *, GtkSelectionData *, guint, gpointer);
|
||||
static void gtk_xtext_clipboard_clear (GtkClipboard *, gpointer);
|
||||
static textentry *gtk_xtext_nth (GtkXText *xtext, int line, int *subline);
|
||||
static void gtk_xtext_adjustment_changed (GtkAdjustment * adj,
|
||||
GtkXText * xtext);
|
||||
@@ -214,6 +312,7 @@ static gboolean gtk_xtext_search_init (xtext_buffer *buf, const gchar *text, gtk
|
||||
static char * gtk_xtext_get_word (GtkXText * xtext, int x, int y, textentry ** ret_ent, int *ret_off, int *ret_len, GSList **slp);
|
||||
static gboolean gtk_xtext_word_select_char (const unsigned char *ch);
|
||||
static gboolean gtk_xtext_get_word_select_range (GtkXText *xtext, int x, int y, textentry **ret_ent, int *ret_off, int *ret_len);
|
||||
static textentry * gtk_xtext_image_hit (GtkXText *xtext, int x, int y);
|
||||
|
||||
static inline void
|
||||
gtk_xtext_cursor_unref (GdkCursor *cursor)
|
||||
@@ -985,9 +1084,29 @@ gtk_xtext_new (const XTextColor *palette, int separator)
|
||||
return GTK_WIDGET (xtext);
|
||||
}
|
||||
|
||||
static void
|
||||
gtk_xtext_release_selections (GtkXText *xtext)
|
||||
{
|
||||
GdkAtom selections[] = { GDK_SELECTION_PRIMARY, GDK_SELECTION_SECONDARY };
|
||||
guint i;
|
||||
|
||||
if (!gtk_widget_has_screen (GTK_WIDGET (xtext)))
|
||||
return;
|
||||
|
||||
for (i = 0; i < G_N_ELEMENTS (selections); i++)
|
||||
{
|
||||
GtkClipboard *clipboard = gtk_widget_get_clipboard (GTK_WIDGET (xtext), selections[i]);
|
||||
|
||||
if (gtk_clipboard_get_owner (clipboard) == G_OBJECT (xtext))
|
||||
gtk_clipboard_clear (clipboard);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
gtk_xtext_cleanup (GtkXText *xtext)
|
||||
{
|
||||
gtk_xtext_release_selections (xtext);
|
||||
|
||||
if (xtext->add_io_tag)
|
||||
{
|
||||
g_source_remove (xtext->add_io_tag);
|
||||
@@ -1064,6 +1183,8 @@ gtk_xtext_dispose (GObject *object)
|
||||
{
|
||||
GtkXText *xtext = GTK_XTEXT (object);
|
||||
|
||||
gtk_xtext_image_pending_forget (gtk_xtext_image_pending_match_xtext, xtext);
|
||||
g_clear_pointer (&xtext->tooltip_url, g_free);
|
||||
gtk_xtext_cleanup (xtext);
|
||||
|
||||
if (G_OBJECT_CLASS (gtk_xtext_parent_class)->dispose)
|
||||
@@ -1080,6 +1201,7 @@ gtk_xtext_finalize (GObject *object)
|
||||
static void
|
||||
gtk_xtext_unrealize (GtkWidget * widget)
|
||||
{
|
||||
gtk_xtext_release_selections (GTK_XTEXT (widget));
|
||||
backend_deinit (GTK_XTEXT (widget));
|
||||
|
||||
/*
|
||||
@@ -1445,7 +1567,7 @@ gtk_xtext_find_x (GtkXText * xtext, int x, textentry * ent, int subline,
|
||||
|
||||
str = ent->str + gtk_xtext_find_subline (xtext, ent, subline);
|
||||
if (str >= ent->str + ent->str_len)
|
||||
return 0;
|
||||
return ent->str_len; /* e.g. the blank rows of an inline image */
|
||||
|
||||
/* Let user select left a few pixels to grab hidden text e.g. '<' */
|
||||
if (x < indent - xtext->space_width)
|
||||
@@ -2145,6 +2267,7 @@ gtk_xtext_get_word (GtkXText * xtext, int x, int y, textentry ** ret_ent,
|
||||
int len;
|
||||
int out_of_bounds = 0;
|
||||
int len_to_offset = 0;
|
||||
gboolean has_hidden;
|
||||
|
||||
ent = gtk_xtext_find_char (xtext, x, y, &offset, &out_of_bounds);
|
||||
if (ent == NULL || out_of_bounds || offset < 0 || offset >= ent->str_len)
|
||||
@@ -2189,10 +2312,15 @@ gtk_xtext_get_word (GtkXText * xtext, int x, int y, textentry ** ret_ent,
|
||||
if (ret_len)
|
||||
*ret_len = len; /* Length before stripping */
|
||||
|
||||
/* a word with hidden text is a placeholder (e.g. an inline image link
|
||||
label); its whole visible part acts as the link */
|
||||
has_hidden = memchr (word, ATTR_HIDDEN, len) != NULL;
|
||||
|
||||
word = gtk_xtext_strip_color (word, len, xtext->scratch_buffer, NULL, slp, FALSE);
|
||||
|
||||
/* avoid turning the cursor into a hand for non-url part of the word */
|
||||
if (xtext->urlcheck_function && xtext->urlcheck_function (GTK_WIDGET (xtext), word))
|
||||
if (!has_hidden &&
|
||||
xtext->urlcheck_function && xtext->urlcheck_function (GTK_WIDGET (xtext), word))
|
||||
{
|
||||
int start, end;
|
||||
url_last (&start, &end);
|
||||
@@ -2276,12 +2404,43 @@ gtk_xtext_unrender_hilight (GtkXText *xtext)
|
||||
xtext->un_hilight = FALSE;
|
||||
}
|
||||
|
||||
/* show (or stop showing) the destination URL of a hovered placeholder
|
||||
word as the widget's tooltip */
|
||||
|
||||
static void
|
||||
gtk_xtext_set_url_tooltip (GtkXText *xtext, const char *url, int url_len)
|
||||
{
|
||||
if (url == NULL)
|
||||
{
|
||||
if (xtext->tooltip_url)
|
||||
{
|
||||
g_free (xtext->tooltip_url);
|
||||
xtext->tooltip_url = NULL;
|
||||
gtk_widget_set_tooltip_text (GTK_WIDGET (xtext), NULL);
|
||||
xtext->tooltip_stamp_set = FALSE;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (xtext->tooltip_url &&
|
||||
strlen (xtext->tooltip_url) == (gsize) url_len &&
|
||||
memcmp (xtext->tooltip_url, url, url_len) == 0)
|
||||
return;
|
||||
|
||||
g_free (xtext->tooltip_url);
|
||||
xtext->tooltip_url = g_strndup (url, url_len);
|
||||
gtk_widget_set_tooltip_text (GTK_WIDGET (xtext), xtext->tooltip_url);
|
||||
xtext->tooltip_stamp_set = FALSE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
gtk_xtext_leave_notify (GtkWidget * widget, GdkEventCrossing * event)
|
||||
{
|
||||
GtkXText *xtext = GTK_XTEXT (widget);
|
||||
GdkWindow *window = gtk_widget_get_window (widget);
|
||||
|
||||
gtk_xtext_set_url_tooltip (xtext, NULL, 0);
|
||||
|
||||
if (xtext->cursor_hand)
|
||||
{
|
||||
gtk_xtext_unrender_hilight (xtext);
|
||||
@@ -2345,6 +2504,7 @@ gtk_xtext_get_word_adjust (GtkXText *xtext, int x, int y, textentry **word_ent,
|
||||
GSList *slp = NULL;
|
||||
unsigned char *word;
|
||||
int word_type = 0;
|
||||
gboolean placeholder = FALSE;
|
||||
|
||||
word = gtk_xtext_get_word (xtext, x, y, word_ent, offset, len, &slp);
|
||||
if (word)
|
||||
@@ -2354,7 +2514,18 @@ gtk_xtext_get_word_adjust (GtkXText *xtext, int x, int y, textentry **word_ent,
|
||||
word_type = xtext->urlcheck_function (GTK_WIDGET (xtext), word);
|
||||
if (word_type > 0)
|
||||
{
|
||||
if (url_last (&laststart, &lastend))
|
||||
/* words with hidden text are placeholders (e.g. inline image
|
||||
links): hilight the whole word, not just the URL inside it */
|
||||
if (memchr ((*word_ent)->str + *offset, ATTR_HIDDEN, *len))
|
||||
{
|
||||
/* the label hides the real destination, so show the full
|
||||
URL as a tooltip while hovering */
|
||||
placeholder = TRUE;
|
||||
if (url_last (&laststart, &lastend) && lastend > laststart)
|
||||
gtk_xtext_set_url_tooltip (xtext,
|
||||
(const char *) word + laststart, lastend - laststart);
|
||||
}
|
||||
else if (url_last (&laststart, &lastend))
|
||||
{
|
||||
int cumlen, startadj = 0, endadj = 0;
|
||||
offlen_t *meta;
|
||||
@@ -2384,6 +2555,9 @@ gtk_xtext_get_word_adjust (GtkXText *xtext, int x, int y, textentry **word_ent,
|
||||
}
|
||||
g_slist_free_full (slp, g_free);
|
||||
|
||||
if (!placeholder)
|
||||
gtk_xtext_set_url_tooltip (xtext, NULL, 0);
|
||||
|
||||
return word_type;
|
||||
}
|
||||
|
||||
@@ -2515,6 +2689,20 @@ gtk_xtext_motion_notify (GtkWidget * widget, GdkEventMotion * event)
|
||||
}
|
||||
|
||||
tooltip_check:
|
||||
/* hovering an inline image? it opens the viewer when clicked */
|
||||
if (gtk_xtext_image_hit (xtext, x, y))
|
||||
{
|
||||
if (xtext->hilight_ent) /* moving here straight from a link */
|
||||
gtk_xtext_leave_notify (widget, NULL);
|
||||
if (!xtext->cursor_hand)
|
||||
{
|
||||
gdk_window_set_cursor (window, xtext->hand_cursor);
|
||||
xtext->cursor_hand = TRUE;
|
||||
xtext->cursor_resize = FALSE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (xtext->buffer->time_stamp && xtext->buffer->indent > 0 && x >= 0 && x < xtext->stamp_width)
|
||||
{
|
||||
textentry *ent = gtk_xtext_find_char (xtext, x, y, NULL, NULL);
|
||||
@@ -2578,8 +2766,16 @@ gtk_xtext_set_clip_owner (GtkWidget * xtext, GdkEventButton * event)
|
||||
{
|
||||
gtk_clipboard_set_text (gtk_widget_get_clipboard (xtext, GDK_SELECTION_CLIPBOARD), str, len);
|
||||
|
||||
gtk_selection_owner_set (xtext, GDK_SELECTION_PRIMARY, event ? event->time : GDK_CURRENT_TIME);
|
||||
gtk_selection_owner_set (xtext, GDK_SELECTION_SECONDARY, event ? event->time : GDK_CURRENT_TIME);
|
||||
/* Let GtkClipboard own the X11 selection window. Making xtext
|
||||
* the selection owner forces its window native and bypasses
|
||||
* GTK's toplevel backing buffer on subsequent redraws.
|
||||
*/
|
||||
gtk_clipboard_set_with_owner (gtk_widget_get_clipboard (xtext, GDK_SELECTION_PRIMARY),
|
||||
gtk_xtext_selection_targets, G_N_ELEMENTS (gtk_xtext_selection_targets),
|
||||
gtk_xtext_clipboard_get, gtk_xtext_clipboard_clear, G_OBJECT (xtext));
|
||||
gtk_clipboard_set_with_owner (gtk_widget_get_clipboard (xtext, GDK_SELECTION_SECONDARY),
|
||||
gtk_xtext_selection_targets, G_N_ELEMENTS (gtk_xtext_selection_targets),
|
||||
gtk_xtext_clipboard_get, gtk_xtext_clipboard_clear, G_OBJECT (xtext));
|
||||
}
|
||||
|
||||
g_free (str);
|
||||
@@ -2622,6 +2818,46 @@ gtk_xtext_unselect (GtkXText *xtext)
|
||||
xtext->buffer->last_ent_end = NULL;
|
||||
}
|
||||
|
||||
/* check whether the given widget coordinates hit an entry's inline image;
|
||||
returns the entry if so */
|
||||
|
||||
static textentry *
|
||||
gtk_xtext_image_hit (GtkXText *xtext, int x, int y)
|
||||
{
|
||||
textentry *ent;
|
||||
int line, subline, image_rows, text_rows, rel_y, img_x;
|
||||
|
||||
if (xtext->fontsize <= 0)
|
||||
return NULL;
|
||||
|
||||
/* adjust y value for negative rounding, double to int */
|
||||
if (y < 0)
|
||||
y -= xtext->fontsize;
|
||||
|
||||
line = (y + xtext->pixel_offset) / xtext->fontsize;
|
||||
ent = gtk_xtext_nth (xtext, line + (int) xtext_adj_get_value (xtext->adj),
|
||||
&subline);
|
||||
if (ent == NULL || ent->image == NULL)
|
||||
return NULL;
|
||||
|
||||
image_rows = gtk_xtext_image_rows (xtext, ent);
|
||||
text_rows = ent->subline_count - image_rows;
|
||||
if (subline < text_rows)
|
||||
return NULL;
|
||||
|
||||
img_x = xtext->buffer->indent + XTEXT_IMAGE_PADDING;
|
||||
if (x < img_x || x >= img_x + gdk_pixbuf_get_width (ent->image))
|
||||
return NULL;
|
||||
|
||||
rel_y = (subline - text_rows) * xtext->fontsize +
|
||||
(y + xtext->pixel_offset) - (line * xtext->fontsize) -
|
||||
XTEXT_IMAGE_PADDING;
|
||||
if (rel_y < 0 || rel_y >= gdk_pixbuf_get_height (ent->image))
|
||||
return NULL;
|
||||
|
||||
return ent;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
gtk_xtext_button_release (GtkWidget * widget, GdkEventButton * event)
|
||||
{
|
||||
@@ -2694,8 +2930,15 @@ gtk_xtext_button_release (GtkWidget * widget, GdkEventButton * event)
|
||||
|
||||
if (!gtk_xtext_is_selecting (xtext))
|
||||
{
|
||||
word = gtk_xtext_get_word (xtext, event_x, event_y, 0, 0, 0, 0);
|
||||
textentry *word_ent = NULL;
|
||||
|
||||
word = gtk_xtext_get_word (xtext, event_x, event_y, &word_ent, 0, 0, 0);
|
||||
xtext->clicked_ent = word ? word_ent : NULL;
|
||||
xtext->clicked_image_ent = word ? NULL :
|
||||
gtk_xtext_image_hit (xtext, event_x, event_y);
|
||||
g_signal_emit (G_OBJECT (xtext), xtext_signals[WORD_CLICK], 0, word ? word : NULL, event);
|
||||
xtext->clicked_ent = NULL;
|
||||
xtext->clicked_image_ent = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2722,14 +2965,16 @@ gtk_xtext_button_press (GtkWidget * widget, GdkEventButton * event)
|
||||
|
||||
if (event->button == 3 || event->button == 2) /* right/middle click */
|
||||
{
|
||||
word = gtk_xtext_get_word (xtext, x, y, 0, 0, 0, 0);
|
||||
word = gtk_xtext_get_word (xtext, x, y, &ent, 0, 0, 0);
|
||||
if (word)
|
||||
{
|
||||
xtext->clicked_ent = ent;
|
||||
g_signal_emit (G_OBJECT (xtext), xtext_signals[WORD_CLICK], 0,
|
||||
word, event);
|
||||
} else
|
||||
g_signal_emit (G_OBJECT (xtext), xtext_signals[WORD_CLICK], 0,
|
||||
"", event);
|
||||
xtext->clicked_ent = NULL;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
@@ -2958,7 +3203,7 @@ gtk_xtext_selection_get (GtkWidget * widget,
|
||||
if (!window || !GDK_IS_WINDOW (window))
|
||||
break;
|
||||
display = gdk_window_get_display (window);
|
||||
if (!display)
|
||||
if (!display || !GDK_IS_X11_DISPLAY (display))
|
||||
break;
|
||||
GdkAtom encoding;
|
||||
gint format;
|
||||
@@ -2983,6 +3228,29 @@ gtk_xtext_selection_get (GtkWidget * widget,
|
||||
g_free (stripped);
|
||||
}
|
||||
|
||||
static void
|
||||
gtk_xtext_clipboard_get (GtkClipboard *clipboard, GtkSelectionData *selection_data,
|
||||
guint info, gpointer owner)
|
||||
{
|
||||
int len;
|
||||
char *text = gtk_xtext_selection_get_text (GTK_XTEXT (owner), &len);
|
||||
|
||||
if (text)
|
||||
{
|
||||
gtk_selection_data_set_text (selection_data, text, len);
|
||||
g_free (text);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
gtk_xtext_clipboard_clear (GtkClipboard *clipboard, gpointer owner)
|
||||
{
|
||||
GtkXText *xtext = GTK_XTEXT (owner);
|
||||
|
||||
if (xtext->orig_buffer)
|
||||
gtk_xtext_selection_kill (xtext, NULL);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
gtk_xtext_scroll (GtkWidget *widget, GdkEventScroll *event)
|
||||
{
|
||||
@@ -4107,6 +4375,7 @@ gtk_xtext_render_line (GtkXText * xtext, textentry * ent, int line,
|
||||
unsigned char *str;
|
||||
int indent, taken, entline, len, y, start_subline;
|
||||
int emphasis = 0;
|
||||
int image_rows;
|
||||
|
||||
entline = taken = 0;
|
||||
str = ent->str;
|
||||
@@ -4177,6 +4446,56 @@ gtk_xtext_render_line (GtkXText * xtext, textentry * ent, int line,
|
||||
}
|
||||
while (str < ent->str + ent->str_len);
|
||||
|
||||
/* draw the inline image on its blank rows below the text. 'subline'
|
||||
still holds the rows left to skip when the page starts inside the
|
||||
image area (the text loop above only consumes text sublines). */
|
||||
image_rows = gtk_xtext_image_rows (xtext, ent);
|
||||
if (image_rows > 0)
|
||||
{
|
||||
int skip = MIN (subline, image_rows);
|
||||
int rows = image_rows - skip;
|
||||
int first_row_y = (xtext->fontsize * line) - xtext->pixel_offset;
|
||||
int drawn = 0;
|
||||
gboolean render_image = !xtext->dont_render && !xtext->skip_border_fills &&
|
||||
!xtext->render_hilights_only;
|
||||
|
||||
while (rows > 0 && line < lines_max)
|
||||
{
|
||||
y = (xtext->fontsize * line) + xtext->font->ascent - xtext->pixel_offset;
|
||||
if (render_image)
|
||||
{
|
||||
int bg_x = MAX (0, xtext->clip_x);
|
||||
int bg_w = MIN (win_width + MARGIN, xtext->clip_x2) - bg_x;
|
||||
|
||||
if (bg_w > 0)
|
||||
xtext_draw_bg (xtext, bg_x, y - xtext->font->ascent, bg_w, xtext->fontsize);
|
||||
gtk_xtext_draw_sep (xtext, y - xtext->font->ascent);
|
||||
}
|
||||
line++;
|
||||
taken++;
|
||||
rows--;
|
||||
drawn++;
|
||||
}
|
||||
|
||||
if (drawn > 0 && render_image)
|
||||
{
|
||||
/* this may be the shared draw context, so the clip has to be
|
||||
undone before anything else is rendered with it */
|
||||
cairo_t *cr = xtext_create_context (xtext);
|
||||
int img_x = xtext->buffer->indent + XTEXT_IMAGE_PADDING;
|
||||
int img_y = first_row_y - (skip * xtext->fontsize) + XTEXT_IMAGE_PADDING;
|
||||
|
||||
cairo_save (cr);
|
||||
cairo_rectangle (cr, 0, first_row_y, win_width + MARGIN,
|
||||
drawn * xtext->fontsize);
|
||||
cairo_clip (cr);
|
||||
gdk_cairo_set_source_pixbuf (cr, ent->image, img_x, img_y);
|
||||
cairo_paint (cr);
|
||||
cairo_restore (cr);
|
||||
cairo_destroy (cr);
|
||||
}
|
||||
}
|
||||
|
||||
gtk_xtext_draw_marker (xtext, ent, y - xtext->fontsize * (taken + start_subline));
|
||||
|
||||
return taken;
|
||||
@@ -4335,6 +4654,7 @@ gtk_xtext_lines_taken (xtext_buffer *buf, textentry * ent)
|
||||
unsigned char *str;
|
||||
int indent, len;
|
||||
int win_width;
|
||||
int image_rows, i;
|
||||
|
||||
g_slist_free (ent->sublines);
|
||||
ent->sublines = NULL;
|
||||
@@ -4344,21 +4664,27 @@ gtk_xtext_lines_taken (xtext_buffer *buf, textentry * ent)
|
||||
if (win_width >= ent->indent + ent->str_width)
|
||||
{
|
||||
ent->sublines = g_slist_append (ent->sublines, GINT_TO_POINTER (ent->str_len));
|
||||
ent->subline_count = 1;
|
||||
return ent->subline_count;
|
||||
}
|
||||
|
||||
indent = ent->indent;
|
||||
str = ent->str;
|
||||
|
||||
do
|
||||
else
|
||||
{
|
||||
len = find_next_wrap (buf->xtext, ent, str, win_width, indent);
|
||||
ent->sublines = g_slist_append (ent->sublines, GINT_TO_POINTER (str + len - ent->str));
|
||||
indent = buf->indent;
|
||||
str += len;
|
||||
indent = ent->indent;
|
||||
str = ent->str;
|
||||
|
||||
do
|
||||
{
|
||||
len = find_next_wrap (buf->xtext, ent, str, win_width, indent);
|
||||
ent->sublines = g_slist_append (ent->sublines, GINT_TO_POINTER (str + len - ent->str));
|
||||
indent = buf->indent;
|
||||
str += len;
|
||||
}
|
||||
while (str < ent->str + ent->str_len);
|
||||
}
|
||||
while (str < ent->str + ent->str_len);
|
||||
|
||||
/* extra blank rows that make room for an attached inline image;
|
||||
their offset is str_len, i.e. past the end of the text */
|
||||
image_rows = gtk_xtext_image_rows (buf->xtext, ent);
|
||||
for (i = 0; i < image_rows; i++)
|
||||
ent->sublines = g_slist_append (ent->sublines, GINT_TO_POINTER ((gint) ent->str_len));
|
||||
|
||||
ent->subline_count = g_slist_length (ent->sublines);
|
||||
return ent->subline_count;
|
||||
@@ -4781,6 +5107,8 @@ gtk_xtext_kill_ent (xtext_buffer *buffer, textentry *ent)
|
||||
ent->sublines = NULL;
|
||||
ent->subline_count = 0;
|
||||
|
||||
gtk_xtext_ent_free_image (ent);
|
||||
|
||||
g_free (ent);
|
||||
return visible;
|
||||
}
|
||||
@@ -4918,6 +5246,7 @@ gtk_xtext_clear (xtext_buffer *buf, int lines)
|
||||
while (buf->text_first)
|
||||
{
|
||||
next = buf->text_first->next;
|
||||
gtk_xtext_ent_free_image (buf->text_first);
|
||||
g_free (buf->text_first);
|
||||
buf->text_first = next;
|
||||
}
|
||||
@@ -5437,6 +5766,9 @@ gtk_xtext_append_entry (xtext_buffer *buf, textentry * ent, time_t stamp)
|
||||
ent->next = NULL;
|
||||
ent->marks = NULL;
|
||||
ent->subline_count = 0;
|
||||
ent->image = NULL;
|
||||
ent->image_full = NULL;
|
||||
ent->image_url = NULL;
|
||||
|
||||
if (ent->indent < MARGIN)
|
||||
ent->indent = MARGIN; /* 2 pixels is the left margin */
|
||||
@@ -5957,13 +6289,260 @@ gtk_xtext_buffer_free (xtext_buffer *buf)
|
||||
gtk_xtext_search_fini (buf);
|
||||
}
|
||||
|
||||
gtk_xtext_image_pending_forget (gtk_xtext_image_pending_match_buf, buf);
|
||||
|
||||
ent = buf->text_first;
|
||||
while (ent)
|
||||
{
|
||||
next = ent->next;
|
||||
gtk_xtext_ent_free_image (ent);
|
||||
g_free (ent);
|
||||
ent = next;
|
||||
}
|
||||
|
||||
g_free (buf);
|
||||
}
|
||||
|
||||
/* ========================================= */
|
||||
/* ========== INLINE IMAGE API ============= */
|
||||
/* ========================================= */
|
||||
|
||||
/* the entry the last word_click signal was emitted for; only valid while
|
||||
handling that signal */
|
||||
|
||||
textentry *
|
||||
gtk_xtext_get_clicked_entry (GtkXText *xtext)
|
||||
{
|
||||
return xtext->clicked_ent;
|
||||
}
|
||||
|
||||
/* the entry whose inline image the last word_click signal (with a NULL
|
||||
word) landed on; only valid while handling that signal */
|
||||
|
||||
textentry *
|
||||
gtk_xtext_get_clicked_image_entry (GtkXText *xtext)
|
||||
{
|
||||
return xtext->clicked_image_ent;
|
||||
}
|
||||
|
||||
/* check that 'ent' is still an entry of the currently shown buffer; used to
|
||||
validate entry pointers that were remembered across main loop iterations */
|
||||
|
||||
gboolean
|
||||
gtk_xtext_buffer_contains (GtkXText *xtext, textentry *ent)
|
||||
{
|
||||
textentry *walk;
|
||||
|
||||
if (ent == NULL)
|
||||
return FALSE;
|
||||
|
||||
for (walk = xtext->buffer->text_first; walk; walk = walk->next)
|
||||
{
|
||||
if (walk == ent)
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
gboolean
|
||||
gtk_xtext_entry_has_image (GtkXText *xtext, textentry *ent)
|
||||
{
|
||||
return ent != NULL && ent->image != NULL;
|
||||
}
|
||||
|
||||
/* the URL of the entry's attached image, or NULL if it has none */
|
||||
|
||||
const char *
|
||||
gtk_xtext_entry_get_image_url (GtkXText *xtext, textentry *ent)
|
||||
{
|
||||
return ent && ent->image ? ent->image_url : NULL;
|
||||
}
|
||||
|
||||
/* larger version of the entry's attached image, for the viewer */
|
||||
|
||||
GdkPixbuf *
|
||||
gtk_xtext_entry_get_full_image (GtkXText *xtext, textentry *ent)
|
||||
{
|
||||
if (ent == NULL)
|
||||
return NULL;
|
||||
return ent->image_full ? ent->image_full : ent->image;
|
||||
}
|
||||
|
||||
/* the entry's subline count changed (image attached or removed):
|
||||
redo its layout, fix the scrollbar and redraw */
|
||||
|
||||
static void
|
||||
gtk_xtext_image_relayout (GtkXText *xtext, xtext_buffer *buf, textentry *ent)
|
||||
{
|
||||
buf->num_lines -= ent->subline_count;
|
||||
buf->num_lines += gtk_xtext_lines_taken (buf, ent);
|
||||
buf->pagetop_ent = NULL;
|
||||
dontscroll (buf);
|
||||
|
||||
if (xtext->buffer == buf)
|
||||
{
|
||||
gtk_xtext_adjustment_set (buf, TRUE);
|
||||
if (buf->scrollbar_down)
|
||||
xtext_adj_set_value (xtext->adj,
|
||||
xtext_adj_get_upper (xtext->adj) -
|
||||
xtext_adj_get_page_size (xtext->adj));
|
||||
if (gtk_widget_get_realized (GTK_WIDGET (xtext)))
|
||||
gtk_widget_queue_draw (GTK_WIDGET (xtext));
|
||||
}
|
||||
}
|
||||
|
||||
/* scale down (never up) to fit within max_width x max_height */
|
||||
|
||||
static GdkPixbuf *
|
||||
gtk_xtext_image_scale_max (GdkPixbuf *pixbuf, int max_width, int max_height)
|
||||
{
|
||||
int width = gdk_pixbuf_get_width (pixbuf);
|
||||
int height = gdk_pixbuf_get_height (pixbuf);
|
||||
int new_width, new_height;
|
||||
double scale;
|
||||
|
||||
if (width < 1 || height < 1)
|
||||
return NULL;
|
||||
|
||||
if (width <= max_width && height <= max_height)
|
||||
return g_object_ref (pixbuf);
|
||||
|
||||
scale = MIN ((double) max_width / width, (double) max_height / height);
|
||||
new_width = MAX (1, (int) (width * scale));
|
||||
new_height = MAX (1, (int) (height * scale));
|
||||
|
||||
return gdk_pixbuf_scale_simple (pixbuf, new_width, new_height,
|
||||
GDK_INTERP_BILINEAR);
|
||||
}
|
||||
|
||||
/* scale down (never up) to fit the usual text area */
|
||||
|
||||
static GdkPixbuf *
|
||||
gtk_xtext_image_scale (xtext_buffer *buf, GdkPixbuf *pixbuf)
|
||||
{
|
||||
int max_width = XTEXT_IMAGE_MAX_WIDTH;
|
||||
int avail;
|
||||
|
||||
avail = buf->window_width - buf->indent - MARGIN - XTEXT_IMAGE_PADDING * 2;
|
||||
if (avail >= 64 && avail < max_width)
|
||||
max_width = avail;
|
||||
|
||||
return gtk_xtext_image_scale_max (pixbuf, max_width, XTEXT_IMAGE_MAX_HEIGHT);
|
||||
}
|
||||
|
||||
/* register an asynchronous image load for 'ent'. Returns a handle to pass
|
||||
to gtk_xtext_image_load_finish(), or 0 if the entry already has an image
|
||||
or a load underway. */
|
||||
|
||||
guint
|
||||
gtk_xtext_image_load_begin (GtkXText *xtext, textentry *ent, const char *url)
|
||||
{
|
||||
xtext_image_pending *pending;
|
||||
GSList *list;
|
||||
|
||||
if (ent == NULL || ent->image != NULL)
|
||||
return 0;
|
||||
|
||||
for (list = image_pending_list; list; list = list->next)
|
||||
{
|
||||
pending = list->data;
|
||||
if (pending->ent == ent)
|
||||
return 0;
|
||||
}
|
||||
|
||||
pending = g_new0 (xtext_image_pending, 1);
|
||||
pending->id = image_pending_next_id++;
|
||||
if (image_pending_next_id == 0) /* skip 0 on wrap, it means failure */
|
||||
image_pending_next_id = 1;
|
||||
pending->xtext = xtext;
|
||||
pending->buf = xtext->buffer;
|
||||
pending->ent = ent;
|
||||
pending->url = g_strdup (url);
|
||||
image_pending_list = g_slist_prepend (image_pending_list, pending);
|
||||
|
||||
return pending->id;
|
||||
}
|
||||
|
||||
/* complete a load started with gtk_xtext_image_load_begin(). pixbuf may be
|
||||
NULL to abandon the load (e.g. download failed). Returns TRUE if the
|
||||
image was attached, FALSE if it was dropped (failure, or the entry is
|
||||
gone). The caller keeps ownership of the pixbuf. */
|
||||
|
||||
gboolean
|
||||
gtk_xtext_image_load_finish (guint handle, GdkPixbuf *pixbuf)
|
||||
{
|
||||
xtext_image_pending *pending = NULL;
|
||||
GSList *list;
|
||||
GdkPixbuf *scaled;
|
||||
GtkXText *xtext;
|
||||
xtext_buffer *buf;
|
||||
textentry *ent;
|
||||
|
||||
if (handle == 0)
|
||||
return FALSE;
|
||||
|
||||
for (list = image_pending_list; list; list = list->next)
|
||||
{
|
||||
xtext_image_pending *walk = list->data;
|
||||
if (walk->id == handle)
|
||||
{
|
||||
pending = walk;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (pending == NULL) /* entry, buffer or widget died meanwhile */
|
||||
return FALSE;
|
||||
|
||||
xtext = pending->xtext;
|
||||
buf = pending->buf;
|
||||
ent = pending->ent;
|
||||
image_pending_list = g_slist_remove (image_pending_list, pending);
|
||||
|
||||
if (pixbuf == NULL || ent->image != NULL)
|
||||
{
|
||||
g_free (pending->url);
|
||||
g_free (pending);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
scaled = gtk_xtext_image_scale (buf, pixbuf);
|
||||
if (scaled == NULL)
|
||||
{
|
||||
g_free (pending->url);
|
||||
g_free (pending);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
ent->image = scaled;
|
||||
ent->image_full = gtk_xtext_image_scale_max (pixbuf,
|
||||
XTEXT_IMAGE_FULL_MAX_WIDTH, XTEXT_IMAGE_FULL_MAX_HEIGHT);
|
||||
ent->image_url = pending->url; /* transferred */
|
||||
g_free (pending);
|
||||
gtk_xtext_image_relayout (xtext, buf, ent);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* remove an entry's inline image again. Returns FALSE if it had none. */
|
||||
|
||||
gboolean
|
||||
gtk_xtext_image_remove (GtkXText *xtext, textentry *ent)
|
||||
{
|
||||
if (ent == NULL || ent->image == NULL)
|
||||
return FALSE;
|
||||
|
||||
g_object_unref (ent->image);
|
||||
ent->image = NULL;
|
||||
if (ent->image_full)
|
||||
{
|
||||
g_object_unref (ent->image_full);
|
||||
ent->image_full = NULL;
|
||||
}
|
||||
g_free (ent->image_url);
|
||||
ent->image_url = NULL;
|
||||
gtk_xtext_image_relayout (xtext, xtext->buffer, ent);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -192,6 +192,7 @@ struct _GtkXText
|
||||
int hilight_end;
|
||||
time_t tooltip_stamp;
|
||||
unsigned int tooltip_stamp_set:1;
|
||||
char *tooltip_url; /* URL shown while hovering a placeholder */
|
||||
|
||||
guint16 fontwidth[128]; /* each char's width, only the ASCII ones */
|
||||
|
||||
@@ -254,6 +255,9 @@ struct _GtkXText
|
||||
unsigned int force_render:1;
|
||||
unsigned int color_paste:1; /* CTRL was pressed when selection finished */
|
||||
|
||||
textentry *clicked_ent; /* entry under the last word_click, or NULL */
|
||||
textentry *clicked_image_ent; /* entry whose inline image was clicked */
|
||||
|
||||
/* settings/prefs */
|
||||
unsigned int auto_indent:1;
|
||||
unsigned int thinline:1;
|
||||
@@ -309,4 +313,19 @@ void gtk_xtext_buffer_free (xtext_buffer *buf);
|
||||
void gtk_xtext_buffer_show (GtkXText *xtext, xtext_buffer *buf, int render);
|
||||
void gtk_xtext_copy_selection (GtkXText *xtext);
|
||||
|
||||
/* inline image support. A textentry can have one image attached, which is
|
||||
* rendered below its text. Loading is asynchronous: begin() registers the
|
||||
* target entry and returns a handle, finish() attaches the downloaded pixbuf
|
||||
* if the entry still exists. The handle stays valid even if the entry, its
|
||||
* buffer or the widget are destroyed in the meantime. */
|
||||
textentry *gtk_xtext_get_clicked_entry (GtkXText *xtext);
|
||||
textentry *gtk_xtext_get_clicked_image_entry (GtkXText *xtext);
|
||||
gboolean gtk_xtext_buffer_contains (GtkXText *xtext, textentry *ent);
|
||||
gboolean gtk_xtext_entry_has_image (GtkXText *xtext, textentry *ent);
|
||||
const char *gtk_xtext_entry_get_image_url (GtkXText *xtext, textentry *ent);
|
||||
GdkPixbuf *gtk_xtext_entry_get_full_image (GtkXText *xtext, textentry *ent);
|
||||
guint gtk_xtext_image_load_begin (GtkXText *xtext, textentry *ent, const char *url);
|
||||
gboolean gtk_xtext_image_load_finish (guint handle, GdkPixbuf *pixbuf);
|
||||
gboolean gtk_xtext_image_remove (GtkXText *xtext, textentry *ent);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -467,6 +467,9 @@ static const GOptionEntry gopt_entries[] =
|
||||
{"no-auto", 'a', 0, G_OPTION_ARG_NONE, &arg_dont_autoconnect, N_("Don't auto connect to servers"), NULL},
|
||||
{"cfgdir", 'd', 0, G_OPTION_ARG_STRING, &arg_cfgdir, N_("Use a different config directory"), "PATH"},
|
||||
{"no-plugins", 'n', 0, G_OPTION_ARG_NONE, &arg_skip_plugins, N_("Don't auto load any plugins"), NULL},
|
||||
#ifdef __linux__
|
||||
{"run-as-root", 0, 0, G_OPTION_ARG_NONE, &arg_run_as_root, N_("Allow ZoiteChat to run as root (unsafe)"), NULL},
|
||||
#endif
|
||||
{"plugindir", 'p', 0, G_OPTION_ARG_NONE, &arg_show_autoload, N_("Show plugin/script auto-load directory"), NULL},
|
||||
{"configdir", 'u', 0, G_OPTION_ARG_NONE, &arg_show_config, N_("Show user config directory"), NULL},
|
||||
{"url", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &arg_url, N_("Open an irc://server:port/channel URL"), "URL"},
|
||||
@@ -475,6 +478,16 @@ static const GOptionEntry gopt_entries[] =
|
||||
{NULL}
|
||||
};
|
||||
|
||||
#ifdef __linux__
|
||||
void
|
||||
fe_root_warning (void)
|
||||
{
|
||||
fputs ("ZoiteChat refuses to run as root on Linux.\n"
|
||||
"Run it as a normal user, or pass --run-as-root to override this protection.\n",
|
||||
stderr);
|
||||
}
|
||||
#endif
|
||||
|
||||
int
|
||||
fe_args (int argc, char *argv[])
|
||||
{
|
||||
|
||||
@@ -90,6 +90,7 @@
|
||||
<MSWindowsTheme Include="$(DepsRoot)\share\themes\MS-Windows\**\*" />
|
||||
<HicolorIcons Include="$(DepsRoot)\share\icons\hicolor\index.theme" />
|
||||
<HicolorIcons Include="$(DepsRoot)\share\icons\hicolor\**\*.*" />
|
||||
<PythonAdditiveApis Include="..\..\plugins\python\_zoitechat_*.py" />
|
||||
</ItemGroup>
|
||||
|
||||
<Copy SourceFiles="@(None)" DestinationFolder="$(ZoiteChatRel)" />
|
||||
@@ -115,6 +116,7 @@
|
||||
<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'" />
|
||||
<Copy SourceFiles="@(PythonAdditiveApis)" DestinationFolder="$(ZoiteChatRel)\python" Condition="('$(Python3Enabled)'=='true' or '$(Python38Enabled)'=='true') and '@(PythonAdditiveApis)' != ''" />
|
||||
|
||||
<WriteLinesToFile File="$(ZoiteChatRel)portable-mode" Lines="2" Overwrite="true" />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user