Tkabber Wiki

Нетривиальные настройки
Login

Материал из Tkabber Wiki

Содержание

Вступление

Эта статья является в некотором роде парной статье Патчи; отличается она тем, что приведённые тут решения не требуют изменения кода Ткаббера, что, несомненно, является для большинства применений более удачным решением.

Важно понимать, что "нетривиальность" настроек означает прежде всего то, что большинство из них использует недокументированные "внутренности" Ткаббера, и это означает, что эти настройки могут "сломаться" новым релизом Ткаббера (или даже при следующем обновлении из репозитория, если вы следите за разработкой).

Страшного, впрочем, ничего нет, так как, во-первых, за подобную гибкость приходится платить, а во-вторых,в новых релизах зачастую меняются и документированные интерфейсы. А уж стабильность нестабильных версий и обсуждать странно. Вообще же не стоит воспринимать только что изложенное слишком серьёзно: просто имейте в виду возможные затруднения. На самом деле изменения, затрагивающие работу приведённых здесь настроек, производятся крайне редко.

Статья пытается достичь двух целей:

Если вы только подступаетесь к нетривиальной конфигурации Ткаббера, потрудитесь изучить эту статью. Там же можно найти информацию о подходах к тестированию новой конфиругации. Если же и само существование/местоположение файла конфигурации Ткаббера для вас неочевидно, начните отсюда.

Часто задаётся вопрос: что означает выражение "впишите в файл конфигурации вне хуков ..."? Оно означает, что приведённый кусок кода должен находиться "на верхнем уровне" файла конфигурации, или — можно сказать и так — вне любого блока, ограниченного {}. Не забывайте: приведённый в рецепте код скорее всего не будет иметь смысла внутри какого-либо хука, то есть не следует допускать таких записей:

hook::add some_hook {
    ...
    # тут -- ваш кусок кода:
    hook::add some_other_hook {
        ...
    }
}

убедитесь, что вы поместили код на "чистое место":

hook::add some_hook {
    ...
}

# тут -- ваш кусок кода:
hook::add some_other_hook {
    ...
}

Ещё один часто всплывающий вопрос касается привязки комбинаций клавиш — под Windows привязки на буквенно-цифровые клавиши работают только на английской раскладке. Эта проблема подробно рассмотрена здесь.

Глобальные Горячие Клавиши (Windows only)

Для разминки давайте прикрутим к Ткабберу стандартную возможность задавать горячие клавиши (hotkeys) так, чтобы они действовали независимо от того, активно окно Ткаббера в данный момент или нет. Дальше по тексту такие hotkeys мы будем называть "Глобальные Горячие Клавиши" - ГГК (от "global hotkeys").

Поддержку ГГК можно реализовать только под Windows, где такая концепция существует на уровне ОС.

Однако, Tk в Windows самостоятельно не поддерживает ГГК, поэтому необходимо скачать библиотеку Winutils. После чего распакуйте эту библиотеку в поддиректорию tkabber/winutil/ в вашей директории с исходниками Ткаббера (то, что называется $::rootdir в Ткаббере). Для примера, на том же уровне иерархии находятся директории ifaceck и ifacetk, к которым мы ещё вернёмся в следующем патче.

Для того, чтобы эта библиотека подключилась на следующей загрузке вашего Ткаббера, необходимо добавить следующие строчки в ваш config.tcl (файл настроек):

if {[file exists $rootdir/winutil/winutils.tcl]} {
    # This will load the neccessary dll on start up
    source $rootdir/winutil/winutils.tcl
}

Теперь необходимо настроить горячие клавиши. Для этого опять откройте на редактирование файл config.tcl, найдите в нём секцию hook::add finload_hook и вставьте в её конец следующие строки:

  winutils::hotkey #auto Ctrl+Shift+I {
      switch -- [wm state .] {
          normal  { wm state . iconic }
          default { wm state . normal }
      }
  }

То есть, если у вас ранее не было такой секции либо она была пустой, полный кусок кода должен выглядеть так:

hook::add finload_hook {
    winutils::hotkey #auto Ctrl+Shift+I {
        switch -- [wm state .] {
            normal  { wm state . iconic }
            default { wm state . normal }
        }
    }
}

После чего сохраните файл config.ctl и перезагрузите Ткаббер. Если всё сделано правильно, то вы сможете скрывать или открывать главное окно программы, нажимая Ctrl-Shift-I (как это сделано в Миранде). В принципе ГГК может быть любой, как и действия, которые она должна выполнять.

И наконец, самое важное - выбрать клавиши, которые вам удобны, и действия, которые вам нужны. Воспользуйтесь моим примером (с действием, которое было нужно мне) и настройте те действия, что удобны вам. Не забудьте потом зайти к нам в конференцию xmpp:tkabber@conference.jabber.ru и рассказать, что у вас получилось :) Может быть, кому-нибудь ещё окажется полезен ваш опыт.

Таким образом, ваши действия сводятся к следующим пунктам:

  1. Установить winutil;
  2. Подключить winutil через config.tcl;
  3. Настроить горячие клавиши в config.tcl;
  4. Перезагрузить Ткаббер и наслаждаться ещё одной его возможностью.

Автор — lknight. English variant of this tutorial

Если к моменту, когда вы читаете это описание, линки на файлы/проекты уже устарели, вы можете скачать актуальные на октябрь 2005 файлы тут:

Отладочный лог под Windows

Wish под Windows не имеет открытых потоков stdout и stderr, которые можно куда-либо перенаправить; точнее, эти потоки направлены в консоль процесса wish.

С другой стороны, Ткаббер повсеместно использует для отладки процедуру debugmsg, которая как раз "ругается" на stdout. Поэтому стандартное юниксовое заклинание

tkabber >/tmp/TKABBER_LOG 2>&1

здесь не проходит.

Обходной путь такой.

В начало config.tcl добавляем:

# Отладка (1 -- включена, 0 -- выключена):
if {1} {
    set debug_lvls {hook plugin chat}

    set logfile [file join [file norm ~] .tkabber TKABBER_LOG]

    close stdout
    open $logfile w

    close stderr
    open $logfile a
} else {
    set debug_lvls {}
}

Этот код перенаправляет стандартные потоки вывода в файл

ВАШ_ДОМАШНИЙ_КАТАЛОГ\.tkabber\TKABBER_LOG

Список классов отладочной информации, определённый здесь как

set debug_lvls {hook plugin chat}

естественно, имеет смысл отредактировать под ваши нужды. К примеру, при отладке собственного плагина frobozz можно использовать в его коде команды типа

debugmsg frobozz "foo: $foo; bar: $bar"

и внести frobozz в список классов отладочных сообщений:

lappend debug_lvls frobozz

Подробности по поводу магии переоткрывания стандартных потоков ввода/вывода в тикле описаны здесь.

Стандартные потоки перенаправлял Kostix.

Хоткей для переключения между режимами ростера "все" и "только онлайн"

Ниже приведён кусочек кода, вешающий на комбинацию Ctrl-o циклическое переключение между режимами ростера "все" и "только онлайн".

Забейте в конфиг вне хуков:

bind Text <Control-Key-o> {}
bind . <Control-Key-o> ifacetk::roster::switch_only_online

Измените хоткей Ctrl-o по вкусу согласно этого документа. Если вы выберете комбинацию, отличную от Ctrl-o, то первая команда из двух вам не потребуется (однако смотрите ниже, чтобы подготовиться к возможным сюрпризам).

Пояснения для въедливых:

Комбинации кнопок а-ля Unix shell в текстовых виджетах

Внимание! Данная функциональность реализована готовым плагином.

Тут рассказывается о том, как прикрутить к текстовым виджетам в Ткаббере "юниксшелловые" комбинации кнопок.

Естественно, это не "комбинации кнопок юниксового шелла" в строгом смысле этого понятия, ибо строгого смысла у этого понятия нет, да и само понятие слишком расплывчато. Это — неполное воспроизведение набора команд по уполчанию библиотеки GNU Readline. Если у вас есть под боком система с Readline, документация на неё обычно доступна в формате GNU Texinfo, а вас интересует страница руководства "rluserman". Кроме того, некоторые приведённые команды "взяты" из оболочки bash. Проще говоря, эти настройки подойдут тем, чьи "клавиатурные" рефлексы "заточены" на bash-подобную оболочку, режим вставки Vim'а и прочие похожие вещи.

Приведённые настройки нужно писать в конфиг Ткаббера.

Команды перемещения курсора

По умолчанию работают и не требуют дополнительных настроек следующие команды:

Реализация следующих двух команд использует возможности интроспекции, предоставляемые тиклем — мы заимствуем реализацию нужных нам действий у существующих команд:

Alt-b — переместить курсор на слово влево:

bind Text <Alt-b> [bind Text <Control-Key-Left>]

Alt-f — переместить курсор на слово вправо:

bind Text <Alt-f> [bind Text <Control-Key-Right>]

Примечание: на некоторых системах, возможно, вместо Alt понадобится указывать модификатор Meta. Можно воспользоваться "копипастом", а можно заставить работать тикль:

array set map {
    b Left
    f Right
}
foreach key [array names map] {
    foreach mod {Alt Meta} {
        bind Text <$mod-$key> [bind Text <Control-Key-$map($key)>]
    }
}
unset map key mod

Эта команда создаст четыре биндинга: Alt-b, Meta-b, Alt-f; и Meta-f, привязанные к соответствующим командам.

Команды удаления текста

По умолчанию работают и не требуют дополнительных настроек следующие команды:

Усовершенствуем встроенные возможности несколькими командами.

Данной команды нет в Readline, но она есть в bash:

Ctrl-u — убить текст от начала строки до позиции курсора:

bind Text <Control-u> { %W delete {insert linestart} insert}

Теперь — убиение по границам слов:

Alt-BackSpace — удалить текст от курсора до начала слова, содержащего позицию курсора, или до начала предыдущего слова, если курсор стоит между словами:

bind Text <Alt-Key-BackSpace> { %W delete [tk::TextPrevPos %W insert tcl_startOfPreviousWord] insert }

Ситуация несколько сложнее с Control-w: в руководстве на Readline сказано, что эта команда удаляет текст до предыдущего пробела и таким образом отличается от Alt-BackSpace.

Лично мне (Kostix) эта разница ясна не вполне. Получается, что Ctrl-w не должна стирать "foo", если нажата в ситуации "foo | bar" ("|" отмечает позицию курсора). Но она сотрёт это слово, будучи активирована в bash. Там она работает в точности, как предыдушая команда.

Одним словом, "suggestions are welcome", но здесь предлагается не заморачиваться и повесить на Ctrl-w ту же команду, что и на Alt-BackSpace. Здесь тоже имеет смысл призвать на помощь тикль:

foreach {mod key} {Alt BackSpace Control w} {
    bind Text <$mod-Key-$key> { %W delete ...
}
unset mod key

Alt-d — удалить текст от курсора до конца слова, содержащего позицию курсора, или до конца следующего слова, если курсор стоит между словами:

bind Text <Alt-Key-d> { %W delete insert [::tk::TextNextPos %W insert tcl_endOfWord] }

Эту же команду может выполнять Alt-Del; повесьте на неё ту же команду, что и для Alt-d. И опять пусть за нас работает тикль:

foreach key {d Delete} {
    bind Text <Alt-Key-$key> { ...
}
unset key

Undo/Redo

Сначала — две команды, отменяющие любое последнее действие по редактированию текста:

Ctrl-_ работает в "иксовых" версиях Tk; для добавления такой поддержки в Windows нужно сделать:

event add <<Undo>> <Control-Key-underscore>

Ctrl-x,Ctrl-u добавляется (заодно показывая синтаксис "цепочечных" событий в Tk) так:

event add <<Undo>> <Control-Key-x><Control-Key-u>

Следующая команда — Ctrl-y — не может иметь чёткого эквивалента, поскольку вставляет последний кусок текста, удалённый из строки и помещённый в kill-ring Readline (а у текстовых виджетов Tk нет такой штуки). Однако фактически это действие очень похоже на "Redo", и именно на эту комбинацию и повешено "Redo" в Windows. В "иксовом" Tk Ctrl-y означает "вставить текст из CLIPBOARD". Если есть желание "перебить" его чтоб было как "в виндах" и bash, то:

bind <<Redo>> <Control-Key-y>

Невошедшее

Пару команд GNU Readline мы обошли стороной:

Замечания

Пытливые умы могли заметить, что использовались две команды — bind и event.

Вывод, который следует из такого разделения, прост: те привязки, которые делались к виртуальным событиям, будут работать помимо Text в виджете любого класса, который эти события поддерживает. А те привязки, которые делались к классу Text, будут работать только в текстовых виджетах. То есть не надо ожидать, что они будут работать в однострочных полях ввода (класс Entry) или комбобоксах (это вообще пакет BWidget и класс надо выяснять).

Нужно ли заморачиваться с подобной "тонкой настройкой" нетекстовых виджетов (например, однострочных полей ввода) — вопрос философский. Как говорится, "patches are welcome".

Дидактика

Поскольку цель данной вики заключается не только в том, чтобы вложить в руки пролетариата пар и электричество, но и в том, чтобы вооружить начинающих ткабберин и ткабберистов необходимым знанием для самостоятельного ковыряния Ткаббера, а главное — неуклонно пропагандировать дао этого Jabber-клиента, мы расскажем здесь, как были получены изложенные в данном разделе сведения.

Посему ниже приведены некоторые идеи относительно данного раздела. Перед чтением убедитесь, что вы приняли позу лотоса, левитируя, и ваша сахасрара полностью открыта.

(!) Сделать: написать про основные идеи ковыряния

Примерный план:

...

"More юникс вея" в Ткаббер добавлял Kostix.

Копирование текста по Ctrl-C из окна чата в "иксах"

Ниже приведён кусок кода, который позволяет в Ткаббере "под иксами" копировать текст из окна чата (из того, в котором ведётся протокол чата), используя стандартные клавиатурные комбинации копирования текста.

Проблема с "недопиленным" Ткаббером заключается в том, что окно чата никогда не получает клавиатурный фокус, что необходимо для работы "хоткеев". В Windows комбинация Ctrl-C работает для данного окна потому, что в Windows не поддерживается механизм "PRIMARY selection" — копирование текста из виджета, не получающего клавиатурный фокус было бы в противном случае невозможно.

"Иксовое ограничение" преодолевается следующим образом: на окно ввода текста чата вешается собственный обработчик, который, будучи вызван по любой стандартной (и нестандартной) комбинации клавиш, соответствующей действию "копировать текст", проверяет:

Таким образом, пользователь всегда жмёт хоткей "копировать текст" в окне ввода текста чата, но текст копируется из того окна (ввода текста или протокола), в котором в данный момент есть выделенный текст.

Этот код следует поместить в config.tcl вне любых хуков.

# Customize chat input window bindings:
namespace eval my {}
proc my::tweak_chat_input_window {chatid type} {
    set cw [chat::chat_win $chatid]
    set iw [chat::input_win $chatid]

    bind $iw <<Copy>> [list \
        if {[string equal [selection own] %W]} [bind $iw <<Copy>>] \
        else [list event generate $cw <<Copy>>]
    ]
}
hook::add open_chat_post_hook my::tweak_chat_input_window

Принцип действия (можно не читать):

Для начала мы создаём собственное пространство имён "my". Это необязательный шаг, но он убирает нашу процедуру из глобального пространства имён. Не мешалась чтоб.

Поскольку наша цель — изменить настройку окна ввода текста для каждого создаваемого сеанса чата, нам удобнее всего написать обработчик события open_chat_post_hook, которое генерируется после того, как Ткаббер создал окно "верхнего уровня" для нового сеанса чата. (Про наиболее важные хуки читайте в документации, идущей вместе с Ткаббером — tkabber.html.)

Обработчик хука open_chat_post_hook вызывается с двумя параметрами:

chatid — это "ключ", который позволяет получить доступ к различным компонентам, поддерживающим данный сеанс чата.

Нас интересует доступ к двум окнам: ввода текста и протокола чата. Их имена мы получаем при помощи процедур chat_win и input_win модуля "chat" (см. chats.tcl).

Теперь окна у нас есть, и можно заняться навешиванием обработчика на виртуальное событие <<Copy>>. За подробностями обращайтесь к описаниям команд bind и event.

Обработчик будет состоять из одной команды — if, но так как нам нужно обеспечить подстановку переменных при формировании этой команды, мы не можем использовать {} и используем команду list. (На самом деле в этом коде можно вместо [list ...] использовать "...".)

Код обработчика события <<Copy>> первым делом узнаёт имя окна, владеющего в данный момент выделением текста, при помощи команды selection, и сравнивает это имя с именем окна, для которого выполняется обработчик (тикль подставляет его вместо специального токена %W; на самом деле тут без проблем можно использовать $iw вместо %W).

Если имена окон совпали, то выделением владеет окно ввода чата, и нам нужно выполнить стандартное для него действие, закреплённое за событием <<Copy>>. Поскольку своей командой bind $iw ... мы как раз это действие и переопределяем, нам нужно узнать код текущего обработчика. Это делается при помощи двухаргументной формы команды bind; результат выполнения этой команды и формирует первую ("истинную") ветвь команды if.

Если имена не совпали, то нам стоит не мудрствуя лукаво сделать вид, будто пользователь исхитрился выполнить действие "копировать текст" в окне протокола чата. На помощь приходит умение команды event генерировать произвольные события в произвольном окне. Соответствующий код, опять сформированный при помощи команды list, чтобы разрешить подстановку переменной cw, формирует "ложную" ветвь нашего обработчика.

Информацию о правильном формировании команд тикля (чем отличается {} от "" и [list ...], а также зачем нужны "\" на концах некоторых строк) читайте в "Эндекологии".

Эзотерический биндинг прикручивал Kostix.

Предотвращение запуска нескольких процессов Ткаббера

Задача: сделать так, чтобы нельзя было запустить вторую копию Ткаббера.

Простое решение

Впишите в конфиг вне любых хуков:

if [catch {open $::configdir/lock {WRONLY CREAT EXCL}} fd] exit
puts $fd [pid]; close $fd
hook::add quit_hook { file delete $::configdir/lock }

Работает это следующим образом:

Если Вам кажется, что "тема интерактива не раскрыта", замените первую строчку на такой фрагмент:

if [catch {open $::configdir/lock {WRONLY CREAT EXCL}} fd] {
    wm withdraw .
    tk_messageBox -title Tkabber -message {Уже работаем!}
    exit
}

Фичу прикручивал Kostix.

Идея атомарной проверки существования файла, совмещённой с его открытием, позаимствована отсюда.

Проблема

А если завершение работы Ткаббера было ненормальным?...

Фичу сомнениям подвергал ycbl.

Kostix парирует: ...то тогда можно пойти и удалить оставшийся файл руками.

Вообще, раз уж этот вопрос задаётся второй раз (см. ниже), вот два альтернативных решения:

Альтернатива: TCP сокет

if [catch {socket -myaddr localhost -server {} 6666}] exit

Сия команда пытается открыть серверный TCP-сокет 127.0.0.1:6666; если он уже занят, команда завершается с ошибкой (catch возвращает "истину") и работа интерпретатора тикля завершается (exit). В случае успеха этот порт "занимается" Ткаббером и вторая копия Ткаббера не сможет запуститься по причине, рассмотренной выше.

6666 — это TCP-порт, который пытается открыть Ткаббер. Его следует выбрать по вкусу, помня, однако, что:

Достоинство решения с сокетом — простота и кроссплатформенность, недосток: бессмысленно занятый порт при работе Ткаббера. Кроме того, следует понимать, что выбранный порт может быть занят любой программой ещё до старта Ткаббера, например, его может назначить ядро для исходящего TCP-соединения какой-либо программы.

Альтернатива: Блокировка файла

if {![catch {package require Tclx}]} {
    set fd [open $::configdir/lock w]
    if {![flock -nowait $fd]} {
        wm withdraw .
        tk_messageBox -message {Уже запущен!}
        exit
    }
    puts $fd [pid]; flush $fd
}

Плюсы:

Минусы:

Можете также добавить к этому коду обработчик события quit_hook, аналогичный показанному выше, для удаления лок-файла при нормальном выходе из Ткаббера.

Пища для размышлений

Кроме приведённых решений, возможны и другие варианты, например:

Можно придумать и ещё. Можете почитать обсуждение этих методов, имевшее место в tkabber@conference.jabber.ru.

А вот мнение профессионалов по этому поводу.

Отправка сообщений нажатием Ctrl-Enter

Впишите в конфиг вне всяких хуков:

proc fix_msg_send_key {chatid type} {
    set iw [::chat::input_win $chatid]

    bind $iw <Control-KeyPress-Return> [bind $iw <KeyPress-Return>]
    bind $iw <KeyPress-Return> [bind $iw <Shift-KeyPress-Return>]
    bind $iw <Shift-KeyPress-Return> {}
}

hook::add open_chat_post_hook fix_msg_send_key

Эта процедура запускается после открытия любого окна чата. Действует она в четыре этапа:

  1. Получает имя окна ввода текста для соответствующего сеанса чата;
  2. Привязывает к этому окну на комбинацию Ctrl-Enter то действие, которое привязано к клавише Enter (то есть "отослать сообщение);
  3. Привязывает к этому окну на клавишу Enter то действие, которое привязано к комбинации Shift-Enter (то есть "вставить перевод строки");
  4. Уничтожает привязку к комбинации Shift-Enter.

Привязку кнопок фиксил kostix.

Альтернативное решение — прямой патч на Tkabber.

Отправка сообщений по нажатию NumPad Enter

Добавить в конфиг вне хуков следующий код:

bind all <Key-KP_Enter> {
    set event Return
    if {%s & 0x01} {
	    set event Shift-$event
    }
    if {%s & 0x04} {
	    set event Control-$event
    }
    if {%s & 0x08} {
	    set event Alt-$event
    }
    if {$event == "Return"} {
	    set event Key-$event
    }
    event generate %W <$event>
}

Этот код навешивает реакцию на нажатие клавиши "Enter на дополнительной клавиатуре" на специальный "тэг привязки" "all", обработчики для событий на котором проверяются, если подходящие не найдены на окне, в котором событие произошло (в реальности это несколько сложнее, но для нашего случая сойдёт).

Код обработчика проверяет состояние управляющих клавиш (Shift, Control, Alt) в событии и генерирует в исходном окне аналогичное по смыслу событие, только вместо "Enter на дополнительной клавиатуре" в нём прописана обычная клавиша ввода.

Закрытие табов по Ctrl-W

Впишите в конфиг вне хуков:

hook::add finload_hook {
    bind . <Control-Key-w> [bind . <Control-Key-F4>]
}

Этот тривиальный код привязывает к комбинации Ctrl-w на главном окне Ткаббера действие, привязанное к комбинации Ctrl-F4 на нём же.

Скрытый по умолчанию ростер

Впишите в конфиг вне хуков:

hook::add finload_hook {
    event generate . <<CollapseRoster>>
}

Этот код программно генерирует событие, которое привязано к комбинации клавиш Ctrl-r (действие "скрыть/показать ростер").

Запрет IRC-команды /exec в окнах чата

Некоторые параноидально настроенные граждане интересовались: не может ли "IRC-style" команда чата /exec (действие "выполнить внешнюю программу при помощи команды Tcl exec и подставить то, что она вывела на stdout/stderr, в качестве результата") иметь вредных последствий?

Чтобы запретить эту команду совсем, впишите в конфиг вне хуков:

hook::add postload_hook {
    proc ::plugins::handle_exec_command args {}
    proc ::plugins::exec_command_comps args {}
}

Этот приём определяет две "пустые" процедуры (то есть процедуры с пустым телом, которые ничего не делают, будучи вызванными). Если процедура, имя которой указано в качестве первого аргумента команды proc, существует, таковая удаляется и заменяется новой, определённой остальными аргументами команды proc. Другими словами, подобный приём позволяет "деактивировать" любую процедуру, не удаляя её (что привело бы к ошибкам времени выполнения).

Также обратите внимание на использование особой конвенции — слова "args" в качестве спецификации аргументов: "args" означает "0 или более аргументов", что позволяет при "деактивации" процедуры не задумываться о количестве аргументов, которые она имела в оригинале — "args" обеспечит корректную работу новой версии для любого их числа.

Отмена подсветки текста активного таба

Одному пользователю Ткаббера не понравилось, что виджет NoteBook из пакета BWidget, используемый Ткаббером для создания "интерфейса с табами", изменяет цвет текста активного таба (того, на котором в данный момент находится курсор мыши) на "стандартный". При этом, если в окне данного таба есть непрочитанные сообщения, и цвет текста в табе, соответственно, отличается от стандартного (обычно он синий или красный), это цветовыделение "убивается" "активной подсветкой".

Проблема спорная, но тем не менее, вот решение: поместите в конфиг вне любых хуков следующий код:

proc fix_tab_colors {w page} {
    $w itemconfigure $page -activeforeground \
        [$w itemcget $page -foreground]
}

hook::add finload_hook {
    .nb bindtabs <Any-Enter> [list fix_tab_colors %W]
}

и перезапустите Ткаббер. Теперь при наведении курсора мыши на таб, цвет текста которого отличается от стандартного из-за наличия в соответствующем окне непрочитанных сообщений, цвет текста таба не изменится.

Работает этот код следующим образом:

В результате настройка -activeforeground таба получает текущее значение его настройки -foreground при каждой активизации этого таба. То есть само действие "фичи" — изменения цвета текста активного таба — эффективно отменяется, что и требовалось получить.

Примечание: в данном случае, так как наша процедура используется для обработки событий, происходящих ровно в одном окне, можно вместо %W написать просто .nb — потеряется (ненужная) гибкость, но зато, возможно, код станет несколько понятнее.

Подсветку отменял kostix.

Настройка кнопки "Тема" в групчатах

Чтобы визуально выделить кнопку "Тема" в окнах групчата, можете вставить в файл конфигурации этот код вне любых хуков:

proc fix_topic_button {chatid type} {
    if {![string equal $type "groupchat"]} return

    set b [::chat::winid $chatid].status.mb
    $b config -relief raised -text Опции
}

hook::add open_chat_post_hook fix_topic_button

Код возвращает кнопке стандартную рельефность и меняет её название с "Тема" на "Опции".

Предустановленные описания статусов

Внимание! Доступен плагин аналогичного назначения, предоставляющий более удобный способ работы с предустановками статусов и их описаний.

Что такое описание статуса и зачем нужны предустановленные описания

В нижней части окна Ткаббера есть текстовое поле для ввода описания статуса. Описание статуса — это подробная информация о причине вашего отсутствия (или присутствия, если хотите). Другие пользователи могут видеть это описание рядом с вашим JID и статусом, например, наведя на ваш ник указатель мыши (это поведение зависит от клиента). Если вы хотите установить описание статуса, вы вводите его в упомянутое текстовое поле, после чего устанавливаете статус посредством меню.

Такая процедура не всегда удобна, так как обычно вам приходится по возвращении, перед установкой статуса "Доступен", править описание статуса, изменяя его с чего-то вроде "ушёл на обед" на "могу говорить". Если вы вместе с установкой статуса хотите автоматически устанавливать описание статуса по умолчанию, назначенное данному статусу, читайте этот раздел далее.

Как настроить предустановленные описания

Следующие изменения в файле конфигурации позволяют при установке статуса менять описание статуса на заранее заданное.

  1. Добавьте в config.tcl следующий текст.

    #Предустановленные описания статусов. Заменяйте текст в кавычках на свой.
    set default_text(available) "I am available" # описание статуса 'Доступен'
    set default_text(chat) "Free to chat"        # описание статуса 'Свободен для разговора'
    set default_text(away) "I am away"           # описание статуса 'Отошёл'
    set default_text(xa) "I am far away"         # описание статуса 'Отошёл давно'
    set default_text(dnd) "Do not disturb"       # описание статуса 'Не беспокоить'
    
    proc ::use_default_status_texts {} {
        set m .presence.button.menu
        foreach {status str} $::user_status_list {
            if {![info exists ::default_text($status)]} {
                continue
            }
            switch -- $status {
                invisible -
                unavailable {}
                default {
                    $m entryconfigure $str \
                       -command [join [list [list set textstatus   $::default_text($status)] [list set userstatus $status]] "; "]
                }
            }
        }
    }
    

    Некоторые (или все) строки, начинающиеся с set default\_text, можно закомментировать. В таком случае при установке соответствующего статуса будет установлено описание статуса, введенное в текстовое поле в нижней части окна Ткаббера, то есть, для соответствующей команды меню будет установлено поведение по умолчанию.

  2. Добавьте вызов процедуры use\_default\_status\_texts в процедуру finload. Если такой процедуры нет в файле config.tcl, то создайте её, вставив следующий текст.

    proc finload {} {
        use_default_status_texts
    }
    

Примечание: С приведёнными настройками изменение описания статуса будет происходить только при изменении статуса с помощью меню, вызываемого кнопкой в левом нижнем углу окна Ткаббера. Если вы хотите изменить описание статуса на вписанное в поле вручную, воспользуйтесь командой главного меню Tkabber → Присутствие.

Добавление собственной кнопки на тулбар

Периодически пользователи Ткаббера предлагают добавить на тулбар ещё одну кнопку, выполняющую ту или иную команду, обычно доступную другим способом, например, через главное меню. Поскольку вкусы и потребности у всех разные, разработчики обычно оставляют такие запросы без внимания.

На самом деле добавить свою кнопку на тулбар очень просто, если знать какую именно команду Tcl требуется выполнять по нажатию этой кнопки, а это уже зависит от личных способностей ковыряющихся в исходниках Ткаббера.

В качестве простейшего примера добавим на тулбар кнопку, открывающую окно Raw XML, обычно доступное через пункт главного меню Ткаббера Службы→Инструменты администратора→Открыть окно XML.

Поместите в файл config.tcl вне любых хуков:

hook::add finload_hook {
    set b [[.mainframe gettoolbar 0].bbox add]
    $b configure \
        -highlightthickness 0 \
        -takefocus 0 -relief link \
        -bd $::tk_borderwidth \
        -padx 1 -pady 1 \
        -text XML \
        -helptext "Открыть окно XML" \
        -command ::plugins::rawxml::open_window \
}

Принцип действия (читать необязательно):

Поскольку мы собираемся добавлять кнопку на уже существующий виджет, наш код должен выполняться после того, как элементы GUI созданы. Для этого код нужно реализовать в виде обработчика хука finload_hook.

Первая строка в обработчике добавляет кнопку на тулбар и присваивает её имя переменной с именем "b":

Вторая строка конфигурирует добавленную кнопку:

Что дальше?

Можно использовать опцию -icon вместо опции -text, если у вас есть готовая картинка 24x24x8bpp. Картинка может быть в любом формате, поддерживаемом Ткаббером, однако, "переносимым" является только формат GIF.

Картинка должна быть предварительно загружена при помощи команды image, например:

set img [image create photo -file /path/to/my/icon.gif]
...
$button configure -icon $img ...

Кроме того, в Ткаббере есть готовая процедура, позволяющая добавить кнопку с картинкой более просто, например:

set icon     [image create photo -file smile.gif]
set command  {tk_messageBox -message Cheese!}
set helptext {Says "Cheese!"}
...
ifacetk::add_toolbar_button $icon $command $helptext

Ткаббер использует виджеты ButtonBox и Button из пакета BWidget, соответственно, за подробной информацией о возможностях работы с тулбаром и отдельными кнопками следует обращаться к документации на BWidget.

Собственное меню для киков в групчате

Добавьте этот код в config.tcl вне любых хуков:

set my_kick_reasons {
    "Ya bastard"
    "Go away, bad boy"
    "French connectionUCK OFF!"
    "Чужие здесь не ходят!"
}

proc create_kick_presets_menu {m connid jid} {
    global my_kick_reasons

    menu $m

    foreach reason $my_kick_reasons {
        if {[string length $reason] > 25} {
            set label [string range $reason 0 22]...
        } else {
            set label $reason
        }
        $m add command -label $label \
            -command [list muc::change_item_param \
            {role none} down $connid $jid $reason]
    }

    set m
}

proc add_kick_presets {m connid jid} {
    set km $m.my_kick_menu
    create_kick_presets_menu $km $connid $jid

    $m add separator
    $m add cascade -label "Custom kicks" -menu $km
}

hook::add roster_create_groupchat_user_menu_hook add_kick_presets

Отредактируйте переменную my_kick_reasons по вкусу.

Автоответчик

Внимание! Данный код был преобразован в плагин и несколько улучшен (в частности, появилась иконка для тулбара). Рекомендуется пользоваться им; данная заметка сохранена для истории.

Для некоторых пользователей бывает слишком обременительно изучать детальную информацию о вашем статусе (к примеру, это ваша подруга, которая пользуется ICQ-клиентом и не обращает внимания на значок «отошёл» напротив вашего ника в списке контактов, зато очень обижается, когда никто не отвечает на её реплики). Однако вы всё же хотели бы, чтобы эти пользователи при попытке заговорить с вами узнавали, что в данный момент за компьютером вас нет, и следовательно, диалог не состоится по уважительной причине. Далее в этом разделе описано, как настроить Ткаббер так, чтобы он отвечал на сообщения от определенных пользователей заранее установленным текстом.

Установка

Чтобы установить автоответчик, нужно

  1. в config.tcl добавить вне хуков следующий код:

    # === Answering machine ===
    
    # == Auto answer settings ==
    # Enable auto answer right from the start.
    # (When running, use the auto_answer [yes|no] procedure in
    #  console to turn auto answer on and off.)
    set auto_answ_enabled no
    
    # JIDs for which answering is disabled.
    # Has priority over auto_answ_regex.
    # A value of "^$" means no restrictions.
    set auto_answ_disable_regex "^$"
    # JIDs for which answering is enabled.
    # A value of "" (or ".", or ".*" etc) means for all JIDs
    set auto_answ_regex ""
    set auto_answ_message "Не могу сейчас говорить (отошел от компьютера). Вернусь — отвечу."
    # Changing status to one from this list
    # will automatically turn the auto answer mode on,
    # while entering a status not listed here will turn it off.
    # Comment this variable out to make the answer mode
    # independent of the availability status.
    set auto_answ_status {
         away
         xa
         dnd
    }
    # == end Auto answer settings ==
    
    proc update_auto_answ_button {} {
        # if button not found, don't do anythyng
        if { [[.mainframe gettoolbar 0].bbox index auto_answer] == -1 } {
            return
        }
        if {$::auto_answ_enabled} {
            set txt "Автоответчик вкл."
        } else {
            set txt "Автоответчик выкл."
        }
        [.mainframe gettoolbar 0].bbox itemconfigure auto_answer \
             -command ::auto_answ_toggle  -text $txt -relief link
    }
    
    proc auto_answ_toggle {} {
        # Invert the "enabled" flag
        set ::auto_answ_enabled [expr !$::auto_answ_enabled]
        # reset reply counters
        if { [array exists ::answered] } {
            array unset ::answered
        }
        update_auto_answ_button
    }
    
    # Sends the message ($::auto_answ_message)
    proc answ_process_message {connid from id type is_subject subject body args} {
        variable answered
        # Do not reply if disabled
        if { !$::auto_answ_enabled } {
            return
        }
        # Do not reply to normal messages
        if {$type != "chat" } {
            return
        }
        set chatid [::chat::chatid $connid $from]
        # Do not reply in MUCs
        if { [::chat::is_groupchat $chatid] } {
            return
        }
        set jid [jlib::connection_jid $connid]
        # Just to make sure the program never replies to itself
        if { $jid == $from } {
            return
        }
        # Do not reply if regexp rules are not met
        # Do not reply to empty messages
        if { [regexp -nocase $::auto_answ_disable_regex $from] \
               || ![regexp -nocase $::auto_answ_regex $from] \
               || [string equal $body ""] } {
            return
        }
        # Increment the number of times we replied to this JID
        if { ![info exists answered($from)] } {
            set answered($from) 1
        } else {
            incr answered($from)
        }
        # Stop replying after 3 replies
        if { $answered($from) > 3 } {
            return
        }
        if {[catch { set user [jlib::connection_user $connid] }]} {
            set user ""
        }
        # Send the message
        hook::run chat_send_message_hook \
            $chatid $user $::auto_answ_message "chat"
    }
    
    # Turns auto answer on or off when status changes
    proc answ_on_status_change {name1 name2 op} {
        if { ![info exists ::auto_answ_status] } {
            return
        }
        # Enable auto answer if current status is in the $::answ_status list
        if { [lsearch -exact $::auto_answ_status $::userstatus] != -1 } {
            # Don't do anything if auto answer is already enabled
            if { $::auto_answ_enabled } {
                return
            }
            auto_answ_toggle
            return
        }
        # Disable auto answer if current status is not in the $::answ_status list
        if { $::auto_answ_enabled } {
            auto_answ_toggle
        }
    }
    
    trace add variable userstatus write answ_on_status_change
    
    hook::add process_message_hook \
        answ_process_message 150
    # === end Answering machine ===
    
  2. В процедуру finload добавить следующие строки.

    #Auto answer button
    [.mainframe gettoolbar 0].bbox add -name auto_answer
    update_auto_answ_button
    

    Если такой процедуры в файле config.tcl нет, создайте её. В итоге она будет выглядеть следующим образом.

    proc finload {} {
        #Auto answer button
        [.mainframe gettoolbar 0].bbox add -name auto_answer
        update_auto_answ_button
    }
    

Настройка

Следующие переменные отвечают за параметры автоответчика.

auto_answ_enabled – если установлена в yes, Ткаббер будет запущен со включенным автоответчиком, если в no, то с выключенным. По умолчанию установлена в no.

auto_answ_disable_regex – регулярное выражение, определяющее JIDы, для которых отсылка сообщений автоответчиком запрещена.

auto_answ_regex – регулярное выражение, определяющее JIDы, для которых отсылка сообщений автоответчиком разрешена.

Переменная auto_answ_disable_regex имеет приоритет над auto_answ_regex, то есть, если JID пользователя, от которого пришло сообщение, соответствует выражению auto_answ_disable_regex, ответное сообщение не будет отправлено несмотря на то, соответствует ли JID выражению auto_answ_regex. Если JID не соответствует ни одному из выражений, сообщение не будет отправлено. Настройки по умолчанию максимально разрешающие, с ними автоматическая отправка ответа будет происходить на любой JID, от которого пришло сообщение.

Примечание: Групчаты являются исключением из этих правил: автоответчик никогда не реагирует на сообщения из них.

auto_answ_message – сообщение, которое получит пользователь, пославший вам сообщение.

auto_answ_status – список статусов, при установке которых автоответчик будет автоматически включаться. При входе в статус, не перечисленный в списке, автоответчик будет отключаться. Закомментируйте эту переменную, чтобы изменение статуса никак не влияло на автоответчик, возможность включать/выключать его вручную остаётся в любом случае. Элементы auto_answ_status принимают значения из списка: available, chat, away, dnd, xa. По умолчанию включение автоответчика настроено для статусов "отошёл", "отошёл давно" и "не беспокоить".

Управление

Если вы всё сделали правильно, на панели инструментов появится кнопка Автоответчик выкл. При ее нажатии автоответчик включается, и надпись на кнопке меняется на Автоответчик вкл.. Чтобы убрать эту кнопку с панели инструментов, закомментируйте строки, добавленные в процедуру finload. В этом случае включать/выключать автоответчик можно с помощью команды auto_answ_toggle в консоли.

Настройка положения окна уведомлений в Windows

Вопрос: Требуется переместить всплывающее окно ("балун", "тултип") с уведомлениями, включающееся опцией Настройки → MainInterface → Systray → ::ifacetk::systray::options(display_status), из верхнего левого угла в какое-то другое место экрана.

Ответ: на самом деле выглядит так, что позиционирование этого окна в Windows вообще говоря сломано, причём — давно, т.к. судя по коду этот тултип должен рисоваться примерно над иконкой в области системных уведомлений (в трее).

Поскольку сломаным оно будет оставаться неизвестно сколько, приведём способ исправить положение ковырянием в конфиге.

Сначала придумайте пару чисел — координаты верхнего левого угла окна уведомлений, в которые по вашей задумке оно должно помещаться при открытии. Можно подойти к этой проблеме и более системно:

Добавьте в конфиг вне любых хуков:

proc systray_tooltip_location args {
    return {ваша_координата_X ваша_координата_Y}
}
hook::add finload_hook {
    set ::ifacetk::systray::.tray(location) ::systray_tooltip_location
}

Можете несколько усовершенствовать код, введя глобальную переменную:

set notification_coords {0 0}

proc systray_tooltip_location args {
    return $::notification_coords
}
hook::add finload_hook {
    set ::ifacetk::systray::.tray(location) ::systray_tooltip_location
}

Теперь вы сможете управлять координатами прямо в ходе работы Ткаббера из консоли, вводя нечто вроде:

set notification_coords {762 670}

Собственный шрифт для ростеров

Шрифт ростеров (списка контактов и ростеров групчатов) настраивается синхронно со шрифтом чата при помощи глобальной переменной font. Если требуется отдельно настраивать шрифты ростеров, придётся править код процедуры ::ifacetk::roster::addline. Для этого воспользуемся испытанным приёмом — динамическим "патченьем" кода Ткаббера.

Напишите в config.tcl вне любых хуков:

# From: [http://wiki.tcl.tk/16978](http://wiki.tcl.tk/16978)
proc params proc {
    set params [list]
    foreach param [info args $proc] {
        if {[info default $proc $param default]} {
            lappend params [list $param $default]
        } else {
            lappend params $param
        }
    }
    return $params
}

hook::add postload_hook {
    set proc ::ifacetk::roster::addline
    set body [info body $proc]
    set body [regsub -- {global\s+\yfont\y} $body {global rosterfont}]
    set body [regsub -all -- {\$font\y} $body {$rosterfont}]
    proc $proc [params $proc] $body
    unset proc body
}

Теперь можно добавить к настройкам шрифтов в том же конфиге нечто вроде

set rosterfont -*-arial-medium-*-*-*-*-*-*-*-*-*-iso10646-*

Перемещение элемента меню ростера комнаты

Некоторые неторопливые пользователи жалуются на то, что при перемещении указателя мыши по контекстном меню ростера комнаты они часто "не дотягивают" до искомого пункта "Показать информацию" и попадают в подменю пункта "Игнорировать", расположенного выше. Приведённые строчки для файла конфигурации перемещают элемент "Игнорировать" в самый низ обсуждаемого меню:

hook::add roster_create_groupchat_user_menu_hook imove 1000

proc imove {m args} {
    set ix [$m index [::msgcat::mc "Ignore"]]
    set cmd [list $m insert end [$m type $ix]]
    set conf [$m entryconfigure $ix]
    foreach item $conf {
        lappend cmd [lindex $item 0] [lindex $item end]
    }
    $m delete $ix
    $m delete $ix ;# separator
    $m insert end separator
    eval $cmd
}

Принцип действия (можно не читать):

Вначале мы добавляем процедуру, которую собираемся написать, в хук, отвечающий за наполнение меню ростера комнаты содержимым. Мы хотим, чтобы наша процедура запустилась последней в ходе отработки хука, поэтому указываем для неё высокий приоритет (1000).

Далее определяем саму процедуру. Первым аргументом она получает имя наполняемого меню. Остальные нас не интересуют и мы используем для них стандартную идиому args (любое число оставшихся аргументов).

Первым делом мы получаем индекс (номер по порядку, начиная с 0) элемента меню, который собираемся переместить. Нас интересует опция "Игнорировать", но поскольку мы хотим написать код, который работает независимо от языка интерфейса, мы ссылаемся на перевод "референсного" названия ("Ignore") при помощи команды msgcat.

Поскольку перемещать элементы меню в Tk нельзя, нам придётся удалить наш элемент, а затем пересоздать его в нужном месте меню. Для этого нам понадобится узнать точную информацию о текущей конфигурации этого элемента, после чего "приготовить" команду, которая сможет воссоздать его в точно таком же состоянии, только в другом месте.

Наша команда (которая будет конструироваться в переменной cmd) начинается "стандартным заклинанием" с участием подкоманды insert виджета menu (a другая его подкоманда — type — возвращает тип переданного ей элемента).

Затем мы получаем полное описание конфигурации данного элемента меню при помощи подкоманды entryconfigure виджета menu. Эта информация возвращается в виде списка — по одному элементу на каждую настройку, определённую для пункта меню; каждый такой элемент, в свою очередь, является списком из пяти элементов: название настройки, имя настройки в базе данных опций Tk, класс настройки в БД опций, значение настройки по умолчанию и, наконец, текущее значение этой настройки. Нас интересуют только первый и последний элементы из перечисленных, так как именно их комбинация определяет текущее состояние виджета. Посему мы итерируем по всем элементам списка конфигурации и добавляем к конструируемой команде имена параметров конфигурации и их текущие значения.

После того, как команда вставки готова, мы удаляем наш элемент меню, сепаратор под ним (это для красоты), после чего вставляем сепаратор в низ меню (опять же для красоты) и, наконец, вставляем в самый низ наш "новый старый" элемент путём выполнения (eval) сконструированной команды.

Кнопка Send (отправить) в окне ввода сообщения

Кнопка Send в правой части окна ввода сообщения в окне разговора позволяет отправлять сообщение с помощью мыши.

Иногда это позволяет сэкономить время, например, когда нужно выделить текст в окне чата, вставить его средней кнопкой в окно ввода и тут же отправить, — все эти действия можно выполнить без применения клавиатуры.

Добавить кнопку Send в окно ввода можно, поместив в файл конфигурации следующий код, регистрирующий обработчик хука open_chat_post_hook.

proc open_chat_post_hook_handler {chatid type} {
    # Send button
    set iw [::chat::input_win $chatid]
    set parent [winfo parent $iw]
    set isw $parent.isw
    set pi [pack info $isw]
    set ix [lsearch -exact $pi -in]; incr ix
    set f [lindex $pi $ix]
    set sb [button $parent.send -text Send -command [list event generate $parent <KeyPress-Return>]]
    pack $isw -in $f -side left
    pack $sb -in $f -side right -fill y
    # End Send button
}

hook::add open_chat_post_hook open_chat_post_hook_handler

Автор кода Kostix.

NB: Если вы воспользовались инструкцией, изложенной в разделе Отправка сообщений нажатием Ctrl-Enter, и добавили в config.tcl соответствующий код, то нужно заменить в приведённой выше процедуре open_chat_post_hook_handler строку <KeyPress-Return> на <Control-KeyPress-Return>.