diff options
author | Xavier Claessens <xclaesse@src.gnome.org> | 2007-04-25 21:59:20 +0000 |
---|---|---|
committer | Xavier Claessens <xclaesse@src.gnome.org> | 2007-04-25 21:59:20 +0000 |
commit | 40c0d1ba5434b5c5d4f8b7ff6fedd1add5c03ce6 (patch) | |
tree | c1426dc8388fc711bf4a5e8de42fd3a4b76e6842 |
[darcs-to-svn @ initial import]
svn path=/trunk/; revision=2
115 files changed, 26980 insertions, 0 deletions
diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 00000000..2847eaa3 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,4 @@ +Mikael Hallendal <micke@imendio.com> +Richard Hult <richard@imendio.com> +Martyn Russell <martyn@gnome.org> +Xavier Claessens <xclaesse@gmail.com> diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 00000000..a9a8cd1f --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1,17 @@ +Andreas Lööw +Aurelien Naldi +Bastien Nocera +Christoffer Olsen +Frederic Crozat +Geert-Jan Van den Bogaerde +Johan Hammar +Jonatan Magnusson +Jordi Mallach +Kim Andersen +Martyn Russell +Mike Gratton +Ross Burton +Sjoerd Simons +Thomas Reynolds +Vincent Untz +Xavier Claessens diff --git a/COPYING b/COPYING new file mode 100644 index 00000000..d60c31a9 --- /dev/null +++ b/COPYING @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 00000000..86b8105e --- /dev/null +++ b/ChangeLog @@ -0,0 +1,4 @@ +2006-03-16 Xavier Claessens <xclaesse@gmail.com> + + * Initial version + diff --git a/MAINTAINERS b/MAINTAINERS new file mode 100644 index 00000000..422a3ed6 --- /dev/null +++ b/MAINTAINERS @@ -0,0 +1 @@ +Xavier Claessens <xclaesse@gmail.com> diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 00000000..bf4cebf4 --- /dev/null +++ b/Makefile.am @@ -0,0 +1,24 @@ +SUBDIRS = po profiles pixmaps libempathy libempathy-gtk launcher accounts contact-list chat + +DISTCHECK_CONFIGURE_FLAGS = \ + --disable-scrollkeeper + +INTLTOOL = \ + intltool-extract.in \ + intltool-merge.in \ + intltool-update.in + +EXTRA_DIST = \ + ChangeLog \ + README \ + CONTRIBUTORS \ + $(INTLTOOL) + +DISTCLEANFILES = \ + intltool-extract \ + intltool-merge \ + intltool-update + +# Workaround broken scrollkeeper that doesn't remove its files on +# uninstall. +distuninstallcheck_listfiles = find . -type f -print | grep -v '^\./var/scrollkeeper' @@ -0,0 +1,15 @@ +How to use empathy ? + +1) Install +$ ./autogen +$ make && make install + +2) Setup and enable some accounts +$ empathy-accounts + +3) Get your contact list +$ empathy-contact-list +This will start MC and connect all enabled accounts. If you start a private chat +or someone is saying something to you, empathy-chat will be started +automagicaly and display a chat UI for your conversation. + @@ -0,0 +1,20 @@ +Things you can do if you want to help: + + - Find a good name, empathy is already used and gaim had legal problems with it. + - Rename all files and functions name to use the new name once we get a cool one. + - Porting gossip-account-widget-*.{c,h} from gossip project (Guillaume is already working on IRC widget). + - Make a EmpathyChatroom object inherited from EmpathyChat and adding support for topic, invite, members list, etc. + - Porting various UI widgets from gossip to libempathy-gtk for contact info, adding contact, personal info, etc. + - GtkWidget-ify gossip widgets imported in libempathy-gtk. Actually most window/dialog do not inherit from GtkWindow/GtkDialog... + - Set up the translation system and import po files from gossip? Or re-translate everything? + - Fix setting subscription for contacts in EmpathyContactList. + - Write a MC plugin to filter channels before dispatching them. For example we need a GtkStatusIcon that blink when an event arrives (text/voip/ft channel) and tells the MC to dispatch the channel only when the user clicked the icon. Like in gossip. + - Testing and Bugfixing. + +SoC projects: + - Adding VoIP support based on the patch proposed for gossip. + - Adding FileTransfer support. + +If you want to contribute you can ask for information at + - #telepathy on freenode + - Telepathy's mailing list. diff --git a/accounts/Makefile.am b/accounts/Makefile.am new file mode 100644 index 00000000..589cae3a --- /dev/null +++ b/accounts/Makefile.am @@ -0,0 +1,19 @@ +AM_CPPFLAGS = \ + -I$(top_srcdir) \ + -DPREFIX="\"$(prefix)"\" \ + -DSYSCONFDIR=\""$(sysconfdir)"\" \ + -DDATADIR=\""$(datadir)"\" \ + -DLIBDIR=\""$(libdir)"\" \ + $(EMPATHY_CFLAGS) \ + $(WARN_CFLAGS) + +bin_PROGRAMS = empathy-accounts + +empathy_accounts_SOURCES = \ + empathy-accounts-main.c + +empathy_accounts_LDADD = \ + $(top_builddir)/libempathy/libempathy.la \ + $(top_builddir)/libempathy-gtk/libempathy-gtk.la \ + $(EMPATHY_LIBS) + diff --git a/accounts/empathy-accounts-main.c b/accounts/empathy-accounts-main.c new file mode 100644 index 00000000..ec30b833 --- /dev/null +++ b/accounts/empathy-accounts-main.c @@ -0,0 +1,59 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2007 Xavier Claessens <xclaesse@gmail.com> + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#include <config.h> + +#include <string.h> +#include <stdlib.h> + +#include <glib.h> +#include <gtk/gtk.h> + +#include <libempathy/empathy-session.h> +#include <libempathy-gtk/gossip-accounts-dialog.h> + +static void +destroy_cb (GtkWidget *dialog, + gpointer user_data) +{ + empathy_session_finalize (); + gtk_main_quit (); +} + +int +main (int argc, char *argv[]) +{ + GtkWidget *dialog; + + gtk_init (&argc, &argv); + + dialog = gossip_accounts_dialog_show (); + + gtk_widget_show (dialog); + g_signal_connect (dialog, "destroy", + G_CALLBACK (destroy_cb), + NULL); + + gtk_main (); + + return EXIT_SUCCESS; +} + diff --git a/acinclude.m4 b/acinclude.m4 new file mode 100644 index 00000000..3120d862 --- /dev/null +++ b/acinclude.m4 @@ -0,0 +1,65 @@ +dnl Turn on the additional warnings last, so -Werror doesn't affect other tests. + +AC_DEFUN([IDT_COMPILE_WARNINGS],[ + if test -f $srcdir/autogen.sh; then + default_compile_warnings="error" + else + default_compile_warnings="no" + fi + + AC_ARG_WITH(compile-warnings, + AS_HELP_STRING([--with-compile-warnings=@<:@no/yes/error@:>@], + [Compiler warnings]), + [enable_compile_warnings="$withval"], + [enable_compile_warnings="$default_compile_warnings"]) + + warnCFLAGS= + if test "x$GCC" != xyes; then + enable_compile_warnings=no + fi + + warning_flags= + realsave_CFLAGS="$CFLAGS" + + case "$enable_compile_warnings" in + no) + warning_flags= + ;; + yes) + warning_flags="-Wall -Wunused -Wmissing-prototypes -Wmissing-declarations" + ;; + maximum|error) + warning_flags="-Wall -Wunused -Wchar-subscripts -Wmissing-declarations -Wmissing-prototypes -Wnested-externs -Wpointer-arith" + CFLAGS="$warning_flags $CFLAGS" + for option in -Wno-sign-compare -Wno-pointer-sign; do + SAVE_CFLAGS="$CFLAGS" + CFLAGS="$CFLAGS $option" + AC_MSG_CHECKING([whether gcc understands $option]) + AC_TRY_COMPILE([], [], + has_option=yes, + has_option=no,) + CFLAGS="$SAVE_CFLAGS" + AC_MSG_RESULT($has_option) + if test $has_option = yes; then + warning_flags="$warning_flags $option" + fi + unset has_option + unset SAVE_CFLAGS + done + unset option + if test "$enable_compile_warnings" = "error" ; then + warning_flags="$warning_flags -Werror" + fi + ;; + *) + AC_MSG_ERROR(Unknown argument '$enable_compile_warnings' to --enable-compile-warnings) + ;; + esac + CFLAGS="$realsave_CFLAGS" + AC_MSG_CHECKING(what warning flags to pass to the C compiler) + AC_MSG_RESULT($warning_flags) + + WARN_CFLAGS="$warning_flags" + AC_SUBST(WARN_CFLAGS) +]) + diff --git a/autogen.sh b/autogen.sh new file mode 100644 index 00000000..1b93b756 --- /dev/null +++ b/autogen.sh @@ -0,0 +1,174 @@ +#!/bin/sh +# Run this to generate all the initial makefiles, etc. + +: ${AUTOCONF=autoconf} +: ${AUTOHEADER=autoheader} +: ${AUTOMAKE=automake-1.9} +: ${ACLOCAL=aclocal-1.9} +: ${LIBTOOLIZE=libtoolize} +: ${INTLTOOLIZE=intltoolize} +: ${LIBTOOL=libtool} +: ${GNOME_DOC_PREPARE=gnome-doc-prepare} + +srcdir=`dirname $0` +test -z "$srcdir" && srcdir=. + +ORIGDIR=`pwd` +cd $srcdir +PROJECT="empathy" +TEST_TYPE=-f +CONFIGURE=configure.ac + +DIE=0 + +($AUTOCONF --version) < /dev/null > /dev/null 2>&1 || { + echo + echo "You must have autoconf installed to compile $PROJECT." + echo "Download the appropriate package for your distribution," + echo "or get the source tarball at ftp://ftp.gnu.org/pub/gnu/" + DIE=1 +} + +(grep "^AC_PROG_INTLTOOL" $srcdir/$CONFIGURE >/dev/null) && { + ($INTLTOOLIZE --version) < /dev/null > /dev/null 2>&1 || { + echo + echo "You must have \`intltoolize' installed to compile $PROJECT." + echo "Get ftp://ftp.gnome.org/pub/GNOME/stable/sources/intltool/intltool-0.35.tar.gz" + echo "(or a newer version if it is available)" + DIE=1 + } +} + +(grep "^GNOME_DOC_INIT" $srcdir/$CONFIGURE >/dev/null) && { + ($GNOME_DOC_PREPARE --version) < /dev/null > /dev/null 2>&1 || { + echo + echo "You must have \`gnome-doc-prepare' installed to compile $PROJECT." + DIE=1 + } +} + +($AUTOMAKE --version) < /dev/null > /dev/null 2>&1 || { + echo + echo "You must have automake 1.9 installed to compile $PROJECT." + echo "Get ftp://ftp.gnu.org/pub/gnu/automake/automake-1.9.6.tar.gz" + echo "(or a newer version of 1.9.x if it is available)" + DIE=1 +} + +(grep "^AM_PROG_LIBTOOL" $CONFIGURE >/dev/null) && { + ($LIBTOOL --version) < /dev/null > /dev/null 2>&1 || { + echo + echo "**Error**: You must have \`libtool' installed to compile $PROJECT." + echo "Get ftp://ftp.gnu.org/pub/gnu/libtool/libtool-1.5.22.tar.gz" + echo "(or a newer version if it is available)" + DIE=1 + } +} + +if grep "^AM_[A-Z0-9_]\{1,\}_GETTEXT" "$CONFIGURE" >/dev/null; then + if grep "sed.*POTFILES" "$CONFIGURE" >/dev/null; then + GETTEXTIZE="" + else + if grep "^AM_GLIB_GNU_GETTEXT" "$CONFIGURE" >/dev/null; then + GETTEXTIZE="glib-gettextize" + GETTEXTIZE_URL="ftp://ftp.gtk.org/pub/gtk/v2.0/glib-2.0.0.tar.gz" + else + GETTEXTIZE="gettextize" + GETTEXTIZE_URL="ftp://alpha.gnu.org/gnu/gettext-0.10.35.tar.gz" + fi + + $GETTEXTIZE --version < /dev/null > /dev/null 2>&1 + if test $? -ne 0; then + echo + echo "**Error**: You must have \`$GETTEXTIZE' installed to compile $PKG_NAME." + echo "Get $GETTEXTIZE_URL" + echo "(or a newer version if it is available)" + DIE=1 + fi + fi +fi + +if test "$DIE" -eq 1; then + exit 1 +fi + +test $TEST_TYPE $FILE || { + echo "You must run this script in the top-level $PROJECT directory" + exit 1 +} + +#if test -z "$*"; then +# echo "I am going to run ./configure with no arguments - if you wish " +# echo "to pass any to it, please specify them on the $0 command line." +#fi + +case $CC in +*xlc | *xlc\ * | *lcc | *lcc\ *) am_opt=--include-deps;; +esac + +for coin in . +do + dr=`dirname $coin` + if test -f $dr/NO-AUTO-GEN; then + echo skipping $dr -- flagged as no auto-gen + else + echo processing $dr + macrodirs= #`sed -n -e 's,AM_ACLOCAL_INCLUDE(\(.*\)),\1,gp' < $coin` + ( cd $dr + aclocalinclude="$ACLOCAL_FLAGS" + for k in $macrodirs; do + if test -d $k; then + aclocalinclude="$aclocalinclude -I $k" + ##else + ## echo "**Warning**: No such directory \`$k'. Ignored." + fi + done + if grep "^AM_GLIB_GNU_GETTEXT" $CONFIGURE >/dev/null; then + if grep "sed.*POTFILES" $CONFIGURE >/dev/null; then + : do nothing -- we still have an old unmodified $CONFIGURE + else + echo "Creating $dr/aclocal.m4 ..." + test -r $dr/aclocal.m4 || touch $dr/aclocal.m4 + echo "Running glib-gettextize... Ignore non-fatal messages." + echo "no" | glib-gettextize --force --copy + echo "Making $dr/aclocal.m4 writable ..." + test -r $dr/aclocal.m4 && chmod u+w $dr/aclocal.m4 + fi + fi + if grep "^IT_PROG_INTLTOOL" $CONFIGURE >/dev/null; then + echo "Running intltoolize..." + intltoolize --copy --force --automake + fi + if grep "^GNOME_DOC_INIT" $CONFIGURE >/dev/null; then + echo "Running $GNOME_DOC_PREPARE..." + $GNOME_DOC_PREPARE --force --copy || exit 1 + fi + if grep "^AM_PROG_LIBTOOL" $CONFIGURE >/dev/null; then + echo "Running $LIBTOOLIZE..." + $LIBTOOLIZE --force --copy + fi + echo "Running $ACLOCAL $aclocalinclude ..." + $ACLOCAL $aclocalinclude + if grep "^AM_CONFIG_HEADER" $CONFIGURE >/dev/null; then + echo "Running $AUTOHEADER..." + $AUTOHEADER + fi + echo "Running $AUTOMAKE --gnu $am_opt ..." + $AUTOMAKE --add-missing --gnu $am_opt + echo "Running $AUTOCONF ..." + $AUTOCONF + ) + fi +done + +conf_flags="--enable-maintainer-mode" + +cd "$ORIGDIR" + +if test x$NOCONFIGURE = x; then + echo Running $srcdir/configure $conf_flags "$@" ... + $srcdir/configure $conf_flags "$@" \ + && echo Now type \`make\' to compile $PROJECT || exit 1 +else + echo Skipping configure process. +fi diff --git a/chat/Makefile.am b/chat/Makefile.am new file mode 100644 index 00000000..d8ee78a0 --- /dev/null +++ b/chat/Makefile.am @@ -0,0 +1,40 @@ +AM_CPPFLAGS = \ + -I$(top_srcdir) \ + -DPREFIX="\"$(prefix)"\" \ + -DSYSCONFDIR=\""$(sysconfdir)"\" \ + -DDATADIR=\""$(datadir)"\" \ + -DLIBDIR=\""$(libdir)"\" \ + $(EMPATHY_CFLAGS) \ + $(WARN_CFLAGS) + +bin_PROGRAMS = empathy-chat + +empathy_chat_SOURCES = \ + empathy-chat-main.c + +empathy_chat_LDADD = \ + $(top_builddir)/libempathy/libempathy.la \ + $(top_builddir)/libempathy-gtk/libempathy-gtk.la \ + $(EMPATHY_LIBS) + +# Dbus service file +servicedir = $(datadir)/dbus-1/services +service_in_files = org.gnome.Empathy.Chat.service.in +service_DATA = $(service_in_files:.service.in=.service) + +# Rule to make the service file with bindir expanded +$(service_DATA): $(service_in_files) Makefile + @sed -e "s|\@bindir\@|$(bindir)|" $< > $@ + +chandlerdir = $(datadir)/telepathy/managers +chandler_DATA = empathy-chat.chandler + + +EXTRA_DIST = \ + org.gnome.Empathy.Chat.service.in \ + $(chandler_DATA) + +BUILT_SOURCES = \ + org.gnome.Empathy.Chat.service + +CLEANFILES = $(BUILT_SOURCES) diff --git a/chat/empathy-chat-main.c b/chat/empathy-chat-main.c new file mode 100644 index 00000000..f2794499 --- /dev/null +++ b/chat/empathy-chat-main.c @@ -0,0 +1,93 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2007 Xavier Claessens <xclaesse@gmail.com> + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#include <config.h> + +#include <stdlib.h> + +#include <glib.h> +#include <gtk/gtk.h> + +#include <libtelepathy/tp-conn.h> +#include <libtelepathy/tp-chan.h> +#include <libmissioncontrol/mc-account.h> + +#include <libempathy/gossip-contact.h> +#include <libempathy/empathy-chandler.h> +#include <libempathy/empathy-session.h> +#include <libempathy/empathy-contact-manager.h> +#include <libempathy/empathy-contact-list.h> +#include <libempathy-gtk/gossip-private-chat.h> +#include <libempathy-gtk/gossip-stock.h> + +#define BUS_NAME "org.gnome.Empathy.Chat" +#define OBJECT_PATH "/org/freedesktop/Telepathy/ChannelHandler" + +static void +new_channel_cb (EmpathyChandler *chandler, + TpConn *tp_conn, + TpChan *tp_chan, + gpointer user_data) +{ + if (tp_chan->handle_type == TP_HANDLE_TYPE_CONTACT) { + MissionControl *mc; + McAccount *account; + EmpathyContactManager *manager; + EmpathyContactList *list; + GossipContact *contact; + GossipPrivateChat *chat; + + /* We have a private chat channel */ + mc = empathy_session_get_mission_control (); + account = mission_control_get_account_for_connection (mc, tp_conn, NULL); + manager = empathy_session_get_contact_manager (); + list = empathy_contact_manager_get_list (manager, account); + contact = empathy_contact_list_get_from_handle (list, tp_chan->handle); + + chat = gossip_private_chat_new_with_channel (contact, tp_chan); + gossip_chat_present (GOSSIP_CHAT (chat)); + + g_object_unref (account); + g_object_unref (contact); + g_object_unref (chat); + } +} + +int +main (int argc, char *argv[]) +{ + EmpathyChandler *chandler; + + gtk_init (&argc, &argv); + /* FIXME: This is a horrible hack */ + gossip_stock_init (gtk_window_new (GTK_WINDOW_TOPLEVEL)); + + chandler = empathy_chandler_new (BUS_NAME, OBJECT_PATH); + + g_signal_connect (chandler, "new-channel", + G_CALLBACK (new_channel_cb), + NULL); + + gtk_main (); + + return EXIT_SUCCESS; +} + diff --git a/chat/empathy-chat.chandler b/chat/empathy-chat.chandler new file mode 100644 index 00000000..4cfe75b0 --- /dev/null +++ b/chat/empathy-chat.chandler @@ -0,0 +1,5 @@ +[ChannelHandler] +BusName = org.gnome.Empathy.Chat +ObjectPath = /org/freedesktop/Telepathy/ChannelHandler +ChannelType = org.freedesktop.Telepathy.Channel.Type.Text + diff --git a/chat/org.gnome.Empathy.Chat.service.in b/chat/org.gnome.Empathy.Chat.service.in new file mode 100644 index 00000000..6a794e4b --- /dev/null +++ b/chat/org.gnome.Empathy.Chat.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=org.gnome.Empathy.Chat +Exec=@bindir@/empathy-chat diff --git a/configure.ac b/configure.ac new file mode 100644 index 00000000..42a16212 --- /dev/null +++ b/configure.ac @@ -0,0 +1,75 @@ +AC_INIT(Empathy, 0.1) +AC_PREREQ(2.59) +AC_COPYRIGHT([Copyright (C) 2003-2007 Imendio AB]) + +AM_CONFIG_HEADER(config.h) +AM_INIT_AUTOMAKE(1.9 dist-bzip2 no-define) + +AM_MAINTAINER_MODE + +AC_ISC_POSIX +AC_PROG_CC +AC_HEADER_STDC + +AM_PROG_LIBTOOL +AM_PATH_GLIB_2_0 +AC_PATH_XTRA + +AC_PATH_PROG(DBUS_BINDING_TOOL, dbus-binding-tool) +AC_PATH_PROG(GCONFTOOL, gconftool-2) +AM_GCONF_SOURCE_2 + +IT_PROG_INTLTOOL([0.35.0]) + +GLIB_REQUIRED=2.12.0 +GTK_REQUIRED=2.10.0 +GCONF_REQUIRED=1.2.0 +LIBGLADE_REQUIRED=2.0.0 +TELEPATHY_REQUIRED=0.0.51 +MISSION_CONTROL_REQUIRED=4.20 + +GLIB_GENMARSHAL=`$PKG_CONFIG glib-2.0 --variable=glib_genmarshal` +AC_SUBST(GLIB_GENMARSHAL) + +IDT_COMPILE_WARNINGS + +dnl ----------------------------------------------------------- +dnl Pkg-Config dependency checks +dnl ----------------------------------------------------------- + +PKG_CHECK_MODULES(EMPATHY, +[ + glib-2.0 >= $GLIB_REQUIRED + gobject-2.0 + gtk+-2.0 >= $GTK_REQUIRED + gconf-2.0 >= $GCONF_REQUIRED + libglade-2.0 >= $LIBGLADE_REQUIRED + libgnomeui-2.0 + libtelepathy >= $TELEPATHY_REQUIRED + libmissioncontrol >= $MISSION_CONTROL_REQUIRED +]) + +dnl ----------------------------------------------------------- +dnl Language Support +dnl ----------------------------------------------------------- +GETTEXT_PACKAGE=empathy +AC_SUBST(GETTEXT_PACKAGE) +AC_DEFINE_UNQUOTED(GETTEXT_PACKAGE,"$GETTEXT_PACKAGE",[Gettext package name]) + +AM_GLIB_GNU_GETTEXT + + +dnl ----------------------------------------------------------- +AC_OUTPUT([ + Makefile + po/Makefile.in + profiles/Makefile + pixmaps/Makefile + libempathy/Makefile + libempathy-gtk/Makefile + launcher/Makefile + accounts/Makefile + contact-list/Makefile + chat/Makefile +]) + diff --git a/contact-list/Makefile.am b/contact-list/Makefile.am new file mode 100644 index 00000000..de678636 --- /dev/null +++ b/contact-list/Makefile.am @@ -0,0 +1,20 @@ +AM_CPPFLAGS = \ + -I$(top_srcdir) \ + -DPREFIX="\"$(prefix)"\" \ + -DSYSCONFDIR=\""$(sysconfdir)"\" \ + -DDATADIR=\""$(datadir)"\" \ + -DLIBDIR=\""$(libdir)"\" \ + $(EMPATHY_CFLAGS) \ + $(WARN_CFLAGS) + +bin_PROGRAMS = empathy-contact-list + +empathy_contact_list_SOURCES = \ + empathy-contact-list-main.c + +empathy_contact_list_LDADD = \ + $(top_builddir)/libempathy/libempathy.la \ + $(top_builddir)/libempathy-gtk/libempathy-gtk.la \ + $(EMPATHY_LIBS) + + diff --git a/contact-list/empathy-contact-list-main.c b/contact-list/empathy-contact-list-main.c new file mode 100644 index 00000000..2f432848 --- /dev/null +++ b/contact-list/empathy-contact-list-main.c @@ -0,0 +1,94 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2007 Xavier Claessens <xclaesse@gmail.com> + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#include <config.h> + +#include <stdlib.h> + +#include <glib.h> +#include <gtk/gtk.h> + +#include <libempathy/gossip-contact.h> +#include <libempathy/empathy-session.h> +#include <libempathy-gtk/gossip-contact-list.h> +#include <libempathy-gtk/gossip-private-chat.h> +#include <libempathy-gtk/gossip-stock.h> + +static void +destroy_cb (GtkWidget *window, + gpointer user_data) +{ + gossip_stock_finalize (); + empathy_session_finalize (); + gtk_main_quit (); +} + +static void +contact_chat_cb (GtkWidget *list, + GossipContact *contact, + gpointer user_data) +{ + mission_control_request_channel (empathy_session_get_mission_control (), + gossip_contact_get_account (contact), + TP_IFACE_CHANNEL_TYPE_TEXT, + gossip_contact_get_handle (contact), + TP_HANDLE_TYPE_CONTACT, + NULL, NULL); +} + +int +main (int argc, char *argv[]) +{ + GtkWidget *window; + GtkWidget *list; + GtkWidget *sw; + + gtk_init (&argc, &argv); + + empathy_session_connect (); + + window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + gossip_stock_init (window); + + list = GTK_WIDGET (gossip_contact_list_new ()); + sw = gtk_scrolled_window_new (NULL, NULL); + gtk_container_add (GTK_CONTAINER (window), sw); + gtk_container_add (GTK_CONTAINER (sw), list); + + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw), + GTK_POLICY_AUTOMATIC, + GTK_POLICY_AUTOMATIC); + gtk_widget_set_size_request (sw, 200, 400); + + g_signal_connect (window, "destroy", + G_CALLBACK (destroy_cb), + NULL); + g_signal_connect (list, "contact-chat", + G_CALLBACK (contact_chat_cb), + NULL); + + gtk_widget_show_all (window); + + gtk_main (); + + return EXIT_SUCCESS; +} + diff --git a/launcher/Makefile.am b/launcher/Makefile.am new file mode 100644 index 00000000..b0662539 --- /dev/null +++ b/launcher/Makefile.am @@ -0,0 +1,12 @@ +INCLUDES = \ + -I$(top_srcdir) \ + $(EMPATHY_CFLAGS) \ + $(WARN_CFLAGS) + +bin_PROGRAMS = empathy-launcher +empathy_launcher_SOURCES = empathy-launcher.c +empathy_launcher_LDADD = \ + $(top_builddir)/libempathy/libempathy.la \ + $(EMPATHY_LIBS) + + diff --git a/launcher/empathy-launcher.c b/launcher/empathy-launcher.c new file mode 100644 index 00000000..44b390fb --- /dev/null +++ b/launcher/empathy-launcher.c @@ -0,0 +1,47 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2007 Xavier Claessens <xclaesse@gmail.com> + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#include <config.h> + +#include <stdlib.h> + +#include <glib.h> + +#include <libempathy/empathy-session.h> +#include <libempathy/gossip-debug.h> + +#define DEBUG_DOMAIN "Launcher" + +int +main (int argc, char *argv[]) +{ + GMainLoop *main_loop; + + g_type_init (); + + empathy_session_connect (); + + main_loop = g_main_loop_new (NULL, FALSE); + g_main_loop_run (main_loop); + + return 0; +} + diff --git a/libempathy-gtk/Makefile.am b/libempathy-gtk/Makefile.am new file mode 100644 index 00000000..16271568 --- /dev/null +++ b/libempathy-gtk/Makefile.am @@ -0,0 +1,68 @@ +AM_CPPFLAGS = \ + -I. \ + -I$(top_srcdir) \ + -DDATADIR=\""$(datadir)"\" \ + -DLOCALEDIR=\""$(datadir)/locale"\" \ + $(EMPATHY_CFLAGS) \ + $(WARN_CFLAGS) + +noinst_LTLIBRARIES = libempathy-gtk.la + +libempathy_gtk_la_SOURCES = \ + gossip-accounts-dialog.c gossip-accounts-dialog.h \ + gossip-account-widget-generic.c gossip-account-widget-generic.h \ + gossip-profile-chooser.c gossip-profile-chooser.h \ + gossip-cell-renderer-expander.c gossip-cell-renderer-expander.h \ + gossip-cell-renderer-text.c gossip-cell-renderer-text.h \ + gossip-stock.c gossip-stock.h \ + gossip-spell.c gossip-spell.h \ + gossip-contact-groups.c gossip-contact-groups.h \ + gossip-contact-list.c gossip-contact-list.h \ + gossip-preferences.c gossip-preferences.h \ + gossip-theme-manager.c gossip-theme-manager.h \ + gossip-chat.c gossip-chat.h \ + gossip-chat-view.c gossip-chat-view.h \ + gossip-chat-window.c gossip-chat-window.h \ + gossip-private-chat.c gossip-private-chat.h \ + gossip-ui-utils.c gossip-ui-utils.h + +libempathy_gtk_la_LIBADD = \ + $(top_builddir)/libempathy/libempathy.la \ + $(EMPATHY_LIBS) + +libempathy_gtk_includedir = $(includedir)/empathy/ + +gladedir = $(datadir)/empathy +glade_DATA = \ + empathy-accounts.glade \ + empathy-chat.glade + +dtddir = $(datadir)/empathy +dtd_DATA = \ + gossip-contact-groups.dtd + +schemasdir = $(GCONF_SCHEMA_FILE_DIR) +schemas_in_files = empathy.schemas.in +schemas_DATA = $(schemas_in_files:.schemas.in=.schemas) +@INTLTOOL_SCHEMAS_RULE@ + +if GCONF_SCHEMAS_INSTALL +install-data-local: + if test -z "$(DESTDIR)" ; then \ + for p in $(schemas_DATA) ; do \ + GCONF_CONFIG_SOURCE=$(GCONF_SCHEMA_CONFIG_SOURCE) $(GCONFTOOL) --makefile-install-rule $(srcdir)/$$p ; \ + done \ + fi +else +install-data-local: +endif + +EXTRA_DIST = \ + $(glade_DATA) \ + $(dtd_DATA) \ + $(schemas_in_files) \ + $(schemas_DATA) + +DISTCLEANFILES = \ + $(schemas_DATA) + diff --git a/libempathy-gtk/empathy-accounts.glade b/libempathy-gtk/empathy-accounts.glade new file mode 100644 index 00000000..a7bb391d --- /dev/null +++ b/libempathy-gtk/empathy-accounts.glade @@ -0,0 +1,843 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd"> +<!--*- mode: xml -*--> +<glade-interface> + <widget class="GtkDialog" id="accounts_dialog"> + <property name="border_width">5</property> + <property name="title" translatable="yes">Accounts</property> + <property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property> + <property name="has_separator">False</property> + <child internal-child="vbox"> + <widget class="GtkVBox" id="dialog-vbox3"> + <property name="visible">True</property> + <property name="spacing">2</property> + <child> + <widget class="GtkHBox" id="hbox146"> + <property name="visible">True</property> + <property name="border_width">6</property> + <property name="spacing">18</property> + <child> + <widget class="GtkVBox" id="vbox195"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <widget class="GtkScrolledWindow" id="scrolledwindow17"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property> + <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property> + <property name="shadow_type">GTK_SHADOW_IN</property> + <child> + <widget class="GtkTreeView" id="treeview"> + <property name="height_request">200</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="enable_search">False</property> + </widget> + </child> + </widget> + </child> + <child> + <widget class="GtkVBox" id="vbox196"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <widget class="GtkButton" id="button_connect"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="label">gtk-connect</property> + <property name="use_stock">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + <child> + <widget class="GtkHBox" id="hbox148"> + <property name="visible">True</property> + <property name="spacing">6</property> + <property name="homogeneous">True</property> + <child> + <widget class="GtkButton" id="button_add"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="label">gtk-add</property> + <property name="use_stock">True</property> + </widget> + </child> + <child> + <widget class="GtkButton" id="button_remove"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="label">gtk-remove</property> + <property name="use_stock">True</property> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + <child> + <widget class="GtkVBox" id="vbox214"> + <property name="width_request">415</property> + <property name="visible">True</property> + <property name="spacing">18</property> + <child> + <widget class="GtkVBox" id="vbox_details"> + <property name="visible">True</property> + <property name="spacing">18</property> + <child> + <widget class="GtkFrame" id="frame1"> + <property name="visible">True</property> + <property name="label_xalign">0</property> + <property name="shadow_type">GTK_SHADOW_NONE</property> + <child> + <widget class="GtkAlignment" id="alignment7"> + <property name="visible">True</property> + <property name="top_padding">6</property> + <property name="left_padding">20</property> + <child> + <widget class="GtkVBox" id="vbox213"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <widget class="GtkTable" id="table14"> + <property name="visible">True</property> + <property name="n_rows">2</property> + <property name="n_columns">2</property> + <property name="column_spacing">6</property> + <property name="row_spacing">6</property> + <child> + <widget class="GtkLabel" id="label_type"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Jabber</property> + </widget> + <packing> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label_name"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Imendio </property> + <property name="width_chars">0</property> + </widget> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkImage" id="image_type"> + <property name="visible">True</property> + <property name="yalign">0</property> + <property name="stock">gtk-cut</property> + <property name="icon_size">6</property> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + </widget> + </child> + </widget> + </child> + </widget> + </child> + <child> + <widget class="GtkLabel" id="label598"> + <property name="visible">True</property> + <property name="label" translatable="yes"><b>Account</b></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="type">label_item</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + <child> + <widget class="GtkFrame" id="frame2"> + <property name="visible">True</property> + <property name="label_xalign">0</property> + <property name="shadow_type">GTK_SHADOW_NONE</property> + <child> + <widget class="GtkAlignment" id="alignment_settings"> + <property name="visible">True</property> + <property name="top_padding">6</property> + <property name="left_padding">20</property> + <child> + <placeholder/> + </child> + </widget> + </child> + <child> + <widget class="GtkLabel" id="label599"> + <property name="visible">True</property> + <property name="label" translatable="yes"><b>Settings</b></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="type">label_item</property> + </packing> + </child> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + </widget> + </child> + <child> + <widget class="GtkFrame" id="frame_new_account"> + <property name="label_xalign">0</property> + <property name="shadow_type">GTK_SHADOW_NONE</property> + <child> + <widget class="GtkAlignment" id="alignment29"> + <property name="visible">True</property> + <property name="top_padding">6</property> + <property name="left_padding">20</property> + <child> + <widget class="GtkVBox" id="vbox216"> + <property name="visible">True</property> + <property name="spacing">12</property> + <child> + <widget class="GtkTable" id="table_new_account"> + <property name="visible">True</property> + <property name="n_rows">2</property> + <property name="n_columns">2</property> + <property name="column_spacing">6</property> + <property name="row_spacing">6</property> + <child> + <placeholder/> + </child> + <child> + <widget class="GtkLabel" id="label638"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">_Name:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">entry_name</property> + </widget> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label640"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">_Type:</property> + <property name="use_underline">True</property> + </widget> + <packing> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkHBox" id="hbox180"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <widget class="GtkEntry" id="entry_name"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="tooltip" translatable="yes">A unique name for this account to identify it personally to you.</property> + <property name="invisible_char">*</property> + </widget> + </child> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkHBox" id="hbox181"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <widget class="GtkButton" id="button_cancel"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="label">gtk-cancel</property> + <property name="use_stock">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="pack_type">GTK_PACK_END</property> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkButton" id="button_create"> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <child> + <widget class="GtkAlignment" id="alignment30"> + <property name="visible">True</property> + <property name="xscale">0</property> + <property name="yscale">0</property> + <child> + <widget class="GtkHBox" id="hbox181"> + <property name="visible">True</property> + <property name="spacing">2</property> + <child> + <widget class="GtkImage" id="image838"> + <property name="visible">True</property> + <property name="stock">gtk-new</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label642"> + <property name="visible">True</property> + <property name="label" translatable="yes">Cr_eate!</property> + <property name="use_underline">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + </child> + </widget> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="pack_type">GTK_PACK_END</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="pack_type">GTK_PACK_END</property> + <property name="position">1</property> + </packing> + </child> + </widget> + </child> + </widget> + </child> + <child> + <widget class="GtkLabel" id="label643"> + <property name="visible">True</property> + <property name="label" translatable="yes"><b>New Account</b></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="type">label_item</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkFrame" id="frame_no_account"> + <property name="label_xalign">0</property> + <property name="shadow_type">GTK_SHADOW_NONE</property> + <child> + <widget class="GtkAlignment" id="alignment21"> + <property name="visible">True</property> + <property name="top_padding">6</property> + <property name="left_padding">12</property> + <child> + <widget class="GtkLabel" id="label_no_account_blurb"> + <property name="visible">True</property> + <property name="label" translatable="yes">To add a new account, you can click on the 'Add' button and a new entry will be created for you to started configuring. + +If you do not want to add an account, simply click on the account you want to configure in the list on the left.</property> + <property name="use_markup">True</property> + <property name="wrap">True</property> + </widget> + </child> + </widget> + </child> + <child> + <widget class="GtkLabel" id="label_no_account"> + <property name="visible">True</property> + <property name="label" translatable="yes"><b>No Account Selected</b></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="type">label_item</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + <child internal-child="action_area"> + <widget class="GtkHButtonBox" id="dialog-action_area"> + <property name="visible">True</property> + <property name="layout_style">GTK_BUTTONBOX_END</property> + <child> + <widget class="GtkButton" id="button_close"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="label">gtk-close</property> + <property name="use_stock">True</property> + <property name="response_id">-6</property> + </widget> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="pack_type">GTK_PACK_END</property> + </packing> + </child> + </widget> + </child> + </widget> + <widget class="GtkWindow" id="account_jabber_settings"> + <property name="visible">True</property> + <property name="title" translatable="yes">jabber account settings</property> + <property name="resizable">False</property> + <child> + <widget class="GtkTable" id="vbox_jabber_settings"> + <property name="visible">True</property> + <property name="n_rows">6</property> + <property name="n_columns">3</property> + <property name="column_spacing">12</property> + <property name="row_spacing">6</property> + <child> + <widget class="GtkLabel" id="label_id"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Login I_D:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">entry_id</property> + </widget> + <packing> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label_password"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Pass_word:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">entry_password</property> + </widget> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label_resource"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Reso_urce:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">entry_resource</property> + </widget> + <packing> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label_server"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">_Server:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">entry_server</property> + </widget> + <packing> + <property name="top_attach">3</property> + <property name="bottom_attach">4</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label_port"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">_Port:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">entry_port</property> + </widget> + <packing> + <property name="top_attach">4</property> + <property name="bottom_attach">5</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkEntry" id="entry_resource"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">*</property> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">3</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkEntry" id="entry_server"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">*</property> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">3</property> + <property name="top_attach">3</property> + <property name="bottom_attach">4</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkEntry" id="entry_port"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">*</property> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">3</property> + <property name="top_attach">4</property> + <property name="bottom_attach">5</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkCheckButton" id="checkbutton_ssl"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="label" translatable="yes">Use encryption (SS_L)</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </widget> + <packing> + <property name="right_attach">3</property> + <property name="top_attach">5</property> + <property name="bottom_attach">6</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkHBox" id="hbox174"> + <property name="visible">True</property> + <property name="spacing">2</property> + <child> + <widget class="GtkEntry" id="entry_password"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="visibility">False</property> + <property name="invisible_char">*</property> + </widget> + </child> + <child> + <widget class="GtkButton" id="button_forget"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="tooltip" translatable="yes">Forget password and clear the entry.</property> + <child> + <widget class="GtkImage" id="image834"> + <property name="visible">True</property> + <property name="stock">gtk-clear</property> + <property name="icon_size">1</property> + </widget> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <widget class="GtkButton" id="button_change_password"> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="label" translatable="yes">C_hange</property> + <property name="use_underline">True</property> + </widget> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkEntry" id="entry_id"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">*</property> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkButton" id="button_register"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="label" translatable="yes">R_egister</property> + <property name="use_underline">True</property> + </widget> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + </widget> + </child> + </widget> + <widget class="GtkWindow" id="account_msn_settings"> + <property name="visible">True</property> + <property name="title" translatable="yes">msn account settings</property> + <property name="resizable">False</property> + <child> + <widget class="GtkTable" id="vbox_msn_settings"> + <property name="visible">True</property> + <property name="n_rows">4</property> + <property name="n_columns">2</property> + <property name="column_spacing">12</property> + <property name="row_spacing">6</property> + <child> + <widget class="GtkLabel" id="label_id"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Login I_D:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">entry_id</property> + </widget> + <packing> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label_password"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Pass_word:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">entry_password</property> + </widget> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkHBox" id="hbox182"> + <property name="visible">True</property> + <property name="spacing">2</property> + <child> + <widget class="GtkEntry" id="entry_password"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="visibility">False</property> + <property name="invisible_char">*</property> + </widget> + </child> + <child> + <widget class="GtkButton" id="button_forget"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="tooltip" translatable="yes">Forget password and clear the entry.</property> + <child> + <widget class="GtkImage" id="image839"> + <property name="visible">True</property> + <property name="stock">gtk-clear</property> + <property name="icon_size">1</property> + </widget> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <widget class="GtkEntry" id="entry_id"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">*</property> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label_server"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">_Server:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">entry_server</property> + </widget> + <packing> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkEntry" id="entry_server"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">*</property> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label_port"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">_Port:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">entry_port</property> + </widget> + <packing> + <property name="top_attach">3</property> + <property name="bottom_attach">4</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkEntry" id="entry_port"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">*</property> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">3</property> + <property name="bottom_attach">4</property> + <property name="y_options"></property> + </packing> + </child> + </widget> + </child> + </widget> +</glade-interface> diff --git a/libempathy-gtk/empathy-chat.glade b/libempathy-gtk/empathy-chat.glade new file mode 100644 index 00000000..e8bd47cf --- /dev/null +++ b/libempathy-gtk/empathy-chat.glade @@ -0,0 +1,700 @@ +<?xml version="1.0" standalone="no"?> <!--*- mode: xml -*--> +<!DOCTYPE glade-interface SYSTEM "http://glade.gnome.org/glade-2.0.dtd"> + +<glade-interface> +<requires lib="gnome"/> + +<widget class="GtkWindow" id="chat_page_window"> + <property name="title" translatable="yes">Chat</property> + <property name="type">GTK_WINDOW_TOPLEVEL</property> + <property name="window_position">GTK_WIN_POS_NONE</property> + <property name="modal">False</property> + <property name="default_width">350</property> + <property name="default_height">250</property> + <property name="resizable">True</property> + <property name="destroy_with_parent">False</property> + <property name="decorated">True</property> + <property name="skip_taskbar_hint">False</property> + <property name="skip_pager_hint">False</property> + <property name="type_hint">GDK_WINDOW_TYPE_HINT_NORMAL</property> + <property name="gravity">GDK_GRAVITY_NORTH_WEST</property> + <property name="focus_on_map">True</property> + <property name="urgency_hint">False</property> + + <child> + <widget class="GtkVBox" id="chat_widget"> + <property name="border_width">4</property> + <property name="visible">True</property> + <property name="homogeneous">False</property> + <property name="spacing">3</property> + + <child> + <widget class="GtkScrolledWindow" id="chat_view_sw"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property> + <property name="vscrollbar_policy">GTK_POLICY_ALWAYS</property> + <property name="shadow_type">GTK_SHADOW_IN</property> + <property name="window_placement">GTK_CORNER_TOP_LEFT</property> + + <child> + <placeholder/> + </child> + </widget> + <packing> + <property name="padding">0</property> + <property name="expand">True</property> + <property name="fill">True</property> + </packing> + </child> + + <child> + <widget class="GtkScrolledWindow" id="input_text_view_sw"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">GTK_POLICY_NEVER</property> + <property name="vscrollbar_policy">GTK_POLICY_NEVER</property> + <property name="shadow_type">GTK_SHADOW_IN</property> + <property name="window_placement">GTK_CORNER_TOP_LEFT</property> + + <child> + <placeholder/> + </child> + </widget> + <packing> + <property name="padding">0</property> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + </widget> + </child> +</widget> + +<widget class="GtkWindow" id="chat_window"> + <property name="title" translatable="yes">Chat</property> + <property name="type">GTK_WINDOW_TOPLEVEL</property> + <property name="window_position">GTK_WIN_POS_NONE</property> + <property name="modal">False</property> + <property name="default_width">350</property> + <property name="default_height">250</property> + <property name="resizable">True</property> + <property name="destroy_with_parent">False</property> + <property name="decorated">True</property> + <property name="skip_taskbar_hint">False</property> + <property name="skip_pager_hint">False</property> + <property name="type_hint">GDK_WINDOW_TYPE_HINT_NORMAL</property> + <property name="gravity">GDK_GRAVITY_NORTH_WEST</property> + <property name="focus_on_map">True</property> + <property name="urgency_hint">False</property> + + <child> + <widget class="GtkVBox" id="chat_vbox"> + <property name="visible">True</property> + <property name="homogeneous">False</property> + <property name="spacing">0</property> + + <child> + <widget class="GtkMenuBar" id="chats_menubar"> + <property name="visible">True</property> + <property name="pack_direction">GTK_PACK_DIRECTION_LTR</property> + <property name="child_pack_direction">GTK_PACK_DIRECTION_LTR</property> + + <child> + <widget class="GtkMenuItem" id="menu_conv"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Conversation</property> + <property name="use_underline">True</property> + + <child> + <widget class="GtkMenu" id="menu_conv_menu"> + + <child> + <widget class="GtkImageMenuItem" id="menu_conv_clear"> + <property name="visible">True</property> + <property name="label" translatable="yes">C_lear</property> + <property name="use_underline">True</property> + + <child internal-child="image"> + <widget class="GtkImage" id="image235"> + <property name="visible">True</property> + <property name="stock">gtk-clear</property> + <property name="icon_size">1</property> + <property name="xalign">0.5</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + </widget> + </child> + </widget> + </child> + + <child> + <widget class="GtkMenuItem" id="menu_conv_insert_smiley"> + <property name="visible">True</property> + <property name="label" translatable="yes">Insert _Smiley</property> + <property name="use_underline">True</property> + </widget> + </child> + + <child> + <widget class="GtkSeparatorMenuItem" id="separator11"> + <property name="visible">True</property> + </widget> + </child> + + <child> + <widget class="GtkImageMenuItem" id="menu_conv_log"> + <property name="visible">True</property> + <property name="label" translatable="yes">_View Previous Conversations</property> + <property name="use_underline">True</property> + <accelerator key="F3" modifiers="0" signal="activate"/> + + <child internal-child="image"> + <widget class="GtkImage" id="image236"> + <property name="visible">True</property> + <property name="stock">gtk-justify-left</property> + <property name="icon_size">1</property> + <property name="xalign">0.5</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + </widget> + </child> + </widget> + </child> + + <child> + <widget class="GtkSeparatorMenuItem" id="menu_conv_separator"> + <property name="visible">True</property> + </widget> + </child> + + <child> + <widget class="GtkImageMenuItem" id="menu_conv_add_contact"> + <property name="label" translatable="yes">_Add Contact...</property> + <property name="use_underline">True</property> + + <child internal-child="image"> + <widget class="GtkImage" id="image237"> + <property name="visible">True</property> + <property name="stock">gtk-add</property> + <property name="icon_size">1</property> + <property name="xalign">0.5</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + </widget> + </child> + </widget> + </child> + + <child> + <widget class="GtkImageMenuItem" id="menu_conv_info"> + <property name="visible">True</property> + <property name="label" translatable="yes">Contact Infor_mation</property> + <property name="use_underline">True</property> + + <child internal-child="image"> + <widget class="GtkImage" id="image238"> + <property name="visible">True</property> + <property name="stock">gtk-info</property> + <property name="icon_size">1</property> + <property name="xalign">0.5</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + </widget> + </child> + </widget> + </child> + + <child> + <widget class="GtkSeparatorMenuItem" id="separator7"> + <property name="visible">True</property> + </widget> + </child> + + <child> + <widget class="GtkImageMenuItem" id="menu_conv_close"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Close</property> + <property name="use_underline">True</property> + <accelerator key="W" modifiers="GDK_CONTROL_MASK" signal="activate"/> + + <child internal-child="image"> + <widget class="GtkImage" id="image239"> + <property name="visible">True</property> + <property name="stock">gtk-close</property> + <property name="icon_size">1</property> + <property name="xalign">0.5</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + </widget> + </child> + </widget> + </child> + </widget> + </child> + </widget> + </child> + + <child> + <widget class="GtkMenuItem" id="menu_room"> + <property name="label" translatable="yes">_Room</property> + <property name="use_underline">True</property> + + <child> + <widget class="GtkMenu" id="menu_room_menu"> + + <child> + <widget class="GtkMenuItem" id="menu_room_set_topic"> + <property name="visible">True</property> + <property name="label" translatable="yes">Change _Topic...</property> + <property name="use_underline">True</property> + </widget> + </child> + + <child> + <widget class="GtkSeparatorMenuItem" id="separator12"> + <property name="visible">True</property> + </widget> + </child> + + <child> + <widget class="GtkImageMenuItem" id="menu_room_join_new"> + <property name="visible">True</property> + <property name="label" translatable="yes">Join _New...</property> + <property name="use_underline">True</property> + + <child internal-child="image"> + <widget class="GtkImage" id="image240"> + <property name="visible">True</property> + <property name="stock">gtk-new</property> + <property name="icon_size">1</property> + <property name="xalign">0.5</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + </widget> + </child> + </widget> + </child> + + <child> + <widget class="GtkMenuItem" id="menu_room_invite"> + <property name="visible">True</property> + <property name="label" translatable="yes">In_vite...</property> + <property name="use_underline">True</property> + </widget> + </child> + + <child> + <widget class="GtkSeparatorMenuItem" id="separator7"> + <property name="visible">True</property> + </widget> + </child> + + <child> + <widget class="GtkImageMenuItem" id="menu_room_add"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Add To Favorites</property> + <property name="use_underline">True</property> + + <child internal-child="image"> + <widget class="GtkImage" id="image241"> + <property name="visible">True</property> + <property name="stock">gtk-add</property> + <property name="icon_size">1</property> + <property name="xalign">0.5</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + </widget> + </child> + </widget> + </child> + + <child> + <widget class="GtkSeparatorMenuItem" id="separator10"> + <property name="visible">True</property> + </widget> + </child> + + <child> + <widget class="GtkCheckMenuItem" id="menu_room_show_contacts"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Show Contacts</property> + <property name="use_underline">True</property> + <property name="active">True</property> + <accelerator key="F11" modifiers="0" signal="activate"/> + </widget> + </child> + </widget> + </child> + </widget> + </child> + + <child> + <widget class="GtkMenuItem" id="menu_edit"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Edit</property> + <property name="use_underline">True</property> + + <child> + <widget class="GtkMenu" id="menu_edit_menu"> + + <child> + <widget class="GtkImageMenuItem" id="menu_edit_cut"> + <property name="visible">True</property> + <property name="label" translatable="yes">Cu_t</property> + <property name="use_underline">True</property> + <accelerator key="X" modifiers="GDK_CONTROL_MASK" signal="activate"/> + + <child internal-child="image"> + <widget class="GtkImage" id="image242"> + <property name="visible">True</property> + <property name="stock">gtk-cut</property> + <property name="icon_size">1</property> + <property name="xalign">0.5</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + </widget> + </child> + </widget> + </child> + + <child> + <widget class="GtkImageMenuItem" id="menu_edit_copy"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Copy</property> + <property name="use_underline">True</property> + <accelerator key="C" modifiers="GDK_CONTROL_MASK" signal="activate"/> + + <child internal-child="image"> + <widget class="GtkImage" id="image243"> + <property name="visible">True</property> + <property name="stock">gtk-copy</property> + <property name="icon_size">1</property> + <property name="xalign">0.5</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + </widget> + </child> + </widget> + </child> + + <child> + <widget class="GtkImageMenuItem" id="menu_edit_paste"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Paste</property> + <property name="use_underline">True</property> + <accelerator key="V" modifiers="GDK_CONTROL_MASK" signal="activate"/> + + <child internal-child="image"> + <widget class="GtkImage" id="image244"> + <property name="visible">True</property> + <property name="stock">gtk-paste</property> + <property name="icon_size">1</property> + <property name="xalign">0.5</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + </widget> + </child> + </widget> + </child> + </widget> + </child> + </widget> + </child> + + <child> + <widget class="GtkMenuItem" id="menu_tabs"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Tabs</property> + <property name="use_underline">True</property> + + <child> + <widget class="GtkMenu" id="menu_tabs_menu"> + + <child> + <widget class="GtkMenuItem" id="menu_tabs_prev"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Previous Tab</property> + <property name="use_underline">True</property> + <accelerator key="Page_Up" modifiers="GDK_CONTROL_MASK" signal="activate"/> + </widget> + </child> + + <child> + <widget class="GtkMenuItem" id="menu_tabs_next"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Next Tab</property> + <property name="use_underline">True</property> + <accelerator key="Page_Down" modifiers="GDK_CONTROL_MASK" signal="activate"/> + </widget> + </child> + + <child> + <widget class="GtkSeparatorMenuItem" id="separator4"> + <property name="visible">True</property> + </widget> + </child> + + <child> + <widget class="GtkMenuItem" id="menu_tabs_left"> + <property name="visible">True</property> + <property name="label" translatable="yes">Move Tab _Left</property> + <property name="use_underline">True</property> + </widget> + </child> + + <child> + <widget class="GtkMenuItem" id="menu_tabs_right"> + <property name="visible">True</property> + <property name="label" translatable="yes">Move Tab _Right</property> + <property name="use_underline">True</property> + </widget> + </child> + + <child> + <widget class="GtkMenuItem" id="menu_tabs_detach"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Detach Tab</property> + <property name="use_underline">True</property> + </widget> + </child> + </widget> + </child> + </widget> + </child> + </widget> + <packing> + <property name="padding">0</property> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + + <child> + <placeholder/> + </child> + </widget> + </child> +</widget> + +<widget class="GtkDialog" id="chat_invite_dialog"> + <property name="border_width">5</property> + <property name="title" translatable="yes">Invite</property> + <property name="type">GTK_WINDOW_TOPLEVEL</property> + <property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property> + <property name="modal">True</property> + <property name="default_width">275</property> + <property name="default_height">225</property> + <property name="resizable">True</property> + <property name="destroy_with_parent">False</property> + <property name="decorated">True</property> + <property name="skip_taskbar_hint">False</property> + <property name="skip_pager_hint">False</property> + <property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property> + <property name="gravity">GDK_GRAVITY_NORTH_WEST</property> + <property name="focus_on_map">True</property> + <property name="urgency_hint">False</property> + <property name="has_separator">False</property> + + <child internal-child="vbox"> + <widget class="GtkVBox" id="vbox6"> + <property name="visible">True</property> + <property name="homogeneous">False</property> + <property name="spacing">0</property> + + <child internal-child="action_area"> + <widget class="GtkHButtonBox" id="hbuttonbox1"> + <property name="visible">True</property> + <property name="layout_style">GTK_BUTTONBOX_END</property> + + <child> + <widget class="GtkButton" id="button_cancel"> + <property name="visible">True</property> + <property name="can_default">True</property> + <property name="can_focus">True</property> + <property name="label">gtk-cancel</property> + <property name="use_stock">True</property> + <property name="relief">GTK_RELIEF_NORMAL</property> + <property name="focus_on_click">True</property> + <property name="response_id">-6</property> + </widget> + </child> + + <child> + <widget class="GtkButton" id="button_invite"> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="can_focus">True</property> + <property name="label">In_vite</property> + <property name="use_underline">True</property> + <property name="relief">GTK_RELIEF_NORMAL</property> + <property name="focus_on_click">True</property> + <property name="response_id">-5</property> + </widget> + </child> + </widget> + <packing> + <property name="padding">0</property> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">GTK_PACK_END</property> + </packing> + </child> + + <child> + <widget class="GtkVBox" id="vbox7"> + <property name="border_width">5</property> + <property name="visible">True</property> + <property name="homogeneous">False</property> + <property name="spacing">18</property> + + <child> + <widget class="GtkVBox" id="vbox7"> + <property name="visible">True</property> + <property name="homogeneous">False</property> + <property name="spacing">6</property> + + <child> + <widget class="GtkLabel" id="label"> + <property name="visible">True</property> + <property name="label" translatable="yes">Select who would you like to invite:</property> + <property name="use_underline">False</property> + <property name="use_markup">True</property> + <property name="justify">GTK_JUSTIFY_LEFT</property> + <property name="wrap">True</property> + <property name="selectable">False</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property> + <property name="width_chars">-1</property> + <property name="single_line_mode">False</property> + <property name="angle">0</property> + </widget> + <packing> + <property name="padding">0</property> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + + <child> + <widget class="GtkScrolledWindow" id="scrolledwindow1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property> + <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property> + <property name="shadow_type">GTK_SHADOW_IN</property> + <property name="window_placement">GTK_CORNER_TOP_LEFT</property> + + <child> + <widget class="GtkTreeView" id="treeview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="headers_visible">False</property> + <property name="rules_hint">False</property> + <property name="reorderable">False</property> + <property name="enable_search">True</property> + <property name="fixed_height_mode">False</property> + <property name="hover_selection">False</property> + <property name="hover_expand">False</property> + </widget> + </child> + </widget> + <packing> + <property name="padding">0</property> + <property name="expand">True</property> + <property name="fill">True</property> + </packing> + </child> + </widget> + <packing> + <property name="padding">0</property> + <property name="expand">True</property> + <property name="fill">True</property> + </packing> + </child> + + <child> + <widget class="GtkVBox" id="vbox8"> + <property name="visible">True</property> + <property name="homogeneous">False</property> + <property name="spacing">6</property> + + <child> + <widget class="GtkLabel" id="label4"> + <property name="visible">True</property> + <property name="label" translatable="yes">Invitation _message:</property> + <property name="use_underline">True</property> + <property name="use_markup">True</property> + <property name="justify">GTK_JUSTIFY_LEFT</property> + <property name="wrap">True</property> + <property name="selectable">False</property> + <property name="xalign">0</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + <property name="mnemonic_widget">entry</property> + <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property> + <property name="width_chars">-1</property> + <property name="single_line_mode">False</property> + <property name="angle">0</property> + </widget> + <packing> + <property name="padding">0</property> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + + <child> + <widget class="GtkEntry" id="entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="editable">True</property> + <property name="visibility">True</property> + <property name="max_length">0</property> + <property name="text" translatable="yes">You have been invited to join a chat conference.</property> + <property name="has_frame">True</property> + <property name="invisible_char">*</property> + <property name="activates_default">True</property> + <property name="width_chars">40</property> + </widget> + <packing> + <property name="padding">0</property> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + </widget> + <packing> + <property name="padding">0</property> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + </widget> + <packing> + <property name="padding">0</property> + <property name="expand">True</property> + <property name="fill">True</property> + </packing> + </child> + </widget> + </child> +</widget> + +</glade-interface> diff --git a/libempathy-gtk/empathy.schemas.in b/libempathy-gtk/empathy.schemas.in new file mode 100644 index 00000000..243e6ce2 --- /dev/null +++ b/libempathy-gtk/empathy.schemas.in @@ -0,0 +1,237 @@ +<gconfschemafile> + <schemalist> + + <schema> + <key>/schemas/apps/empathy/ui/show_offline</key> + <applyto>/apps/empathy/ui/show_offline</applyto> + <owner>empathy</owner> + <type>bool</type> + <default>false</default> + <locale name="C"> + <short>Show offline contacts</short> + <long> + Whether or not to show contacts that are offline in the contact list. + </long> + </locale> + </schema> + + <schema> + <key>/schemas/apps/empathy/ui/show_avatars</key> + <applyto>/apps/empathy/ui/show_avatars</applyto> + <owner>empathy</owner> + <type>bool</type> + <default>true</default> + <locale name="C"> + <short>Show avatars</short> + <long> + Whether or not to show avatars for contacts in the contact + list and chat windows. + </long> + </locale> + </schema> + + <schema> + <key>/schemas/apps/empathy/ui/compact_contact_list</key> + <applyto>/apps/empathy/ui/compact_contact_list</applyto> + <owner>empathy</owner> + <type>bool</type> + <default>false</default> + <locale name="C"> + <short>Compact contact list</short> + <long> + Whether to show the contact list in compact mode or not. + </long> + </locale> + </schema> + + <schema> + <key>/schemas/apps/empathy/ui/main_window_hidden</key> + <applyto>/apps/empathy/ui/main_window_hidden</applyto> + <owner>empathy</owner> + <type>bool</type> + <default>false</default> + <locale name="C"> + <short>Hide main window</short> + <long> + Hide the main window. + </long> + </locale> + </schema> + + <schema> + <key>/schemas/apps/empathy/ui/avatar_directory</key> + <applyto>/apps/empathy/ui/avatar_directory</applyto> + <owner>empathy</owner> + <type>string</type> + <default></default> + <locale name="C"> + <short>Default directory to select an avatar image from</short> + <long> + The last directory that an avatar image was chosen from. + </long> + </locale> + </schema> + + <schema> + <key>/schemas/apps/empathy/notifications/play_sounds</key> + <applyto>/apps/empathy/notifications/play_sounds</applyto> + <owner>empathy</owner> + <type>bool</type> + <default>true</default> + <locale name="C"> + <short>Use notification sounds</short> + <long> + Whether or not to play a sound when messages arrive. + </long> + </locale> + </schema> + + <schema> + <key>/schemas/apps/empathy/notifications/sound_when_away</key> + <applyto>/apps/empathy/notifications/sound_when_away</applyto> + <owner>empathy</owner> + <type>bool</type> + <default>false</default> + <locale name="C"> + <short>Enable sound when away</short> + <long> + Whether or not to play sounds when away. + </long> + </locale> + </schema> + + <schema> + <key>/schemas/apps/empathy/notifications/sound_when_busy</key> + <applyto>/apps/empathy/notifications/sound_when_busy</applyto> + <owner>empathy</owner> + <type>bool</type> + <default>true</default> + <locale name="C"> + <short>Enable sound when busy</short> + <long> + Whether or not to play sounds when busy. + </long> + </locale> + </schema> + + <schema> + <key>/schemas/apps/empathy/notifications/popup_when_contact_available</key> + <applyto>/apps/empathy/notifications/popup_when_contact_available</applyto> + <owner>empathy</owner> + <type>bool</type> + <default>true</default> + <locale name="C"> + <short>Enable popup when contact is available</short> + <long> + Whether or not to show a popup when a contact becomes available. + </long> + </locale> + </schema> + + <schema> + <key>/schemas/apps/empathy/ui/separate_chat_windows</key> + <applyto>/apps/empathy/ui/separate_chat_windows</applyto> + <owner>empathy</owner> + <type>bool</type> + <default>false</default> + <locale name="C"> + <short>Open new chats in separate windows</short> + <long> + Always open a separate chat window for new chats. + </long> + </locale> + </schema> + + <schema> + <key>/schemas/apps/empathy/conversation/graphical_smileys</key> + <applyto>/apps/empathy/conversation/graphical_smileys</applyto> + <owner>empathy</owner> + <type>bool</type> + <default>true</default> + <locale name="C"> + <short>Use graphical smileys</short> + <long> + Whether or not to convert smileys into graphical images in + conversations. + </long> + </locale> + </schema> + + <schema> + <key>/schemas/apps/empathy/conversation/theme</key> + <applyto>/apps/empathy/conversation/theme</applyto> + <owner>empathy</owner> + <type>string</type> + <default>classic</default> + <locale name="C"> + <short>Chat window theme</short> + <long> + The theme that is used to display the conversation in chat windows. + </long> + </locale> + </schema> + + <schema> + <key>/schemas/apps/empathy/conversation/theme_chat_room</key> + <applyto>/apps/empathy/conversation/theme_chat_room</applyto> + <owner>empathy</owner> + <type>bool</type> + <default>true</default> + <locale name="C"> + <short>Use theme for chat rooms</short> + <long> + Whether to use the theme for chat rooms or not. + </long> + </locale> + </schema> + + <schema> + <key>/schemas/apps/empathy/conversation/spell_checker_languages</key> + <applyto>/apps/empathy/conversation/spell_checker_languages</applyto> + <owner>empathy</owner> + <type>string</type> + <default>en</default> + <locale name="C"> + <short>Spell checking languages</short> + <long> + Comma separated list of spell checker languages to use (e.g. en, fr, nl). + </long> + </locale> + </schema> + + <schema> + <key>/schemas/apps/empathy/conversation/spell_checker_enabled</key> + <applyto>/apps/empathy/conversation/spell_checker_enabled</applyto> + <owner>empathy</owner> + <type>bool</type> + <default>true</default> + <locale name="C"> + <short>Enable spell checker</short> + <long> + Whether or not to check words typed against the languages you + want to check with. + </long> + </locale> + </schema> + + <schema> + <key>/schemas/apps/empathy/hints/close_main_window</key> + <applyto>/apps/empathy/hints/close_main_window</applyto> + <owner>empathy</owner> + <type>bool</type> + <default>true</default> + <locale name="C"> + <short>Show hint about closing the main window</short> + <long> + Whether or not to show the message dialog about closing the + main window with the 'x' button in the title bar. + </long> + </locale> + </schema> + + </schemalist> +</gconfschemafile> + + + + diff --git a/libempathy-gtk/gossip-account-widget-generic.c b/libempathy-gtk/gossip-account-widget-generic.c new file mode 100644 index 00000000..7f784639 --- /dev/null +++ b/libempathy-gtk/gossip-account-widget-generic.c @@ -0,0 +1,309 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2006-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Xavier Claessens <xclaesse@gmail.com> + * Martyn Russell <martyn@imendio.com> + */ + +#include <config.h> + +#include <string.h> + +#include <gtk/gtk.h> +#include <glib/gi18n.h> + +#include <libmissioncontrol/mc-account.h> +#include <libmissioncontrol/mc-protocol.h> + +#include <libempathy-gtk/gossip-ui-utils.h> + +#include "gossip-account-widget-generic.h" + +typedef struct { + McAccount *account; + + GtkWidget *sw; + GtkWidget *table_settings; + GtkSizeGroup *size_group; + + guint n_rows; +} GossipAccountWidgetGeneric; + +static gboolean account_widget_generic_entry_focus_cb (GtkWidget *widget, + GdkEventFocus *event, + GossipAccountWidgetGeneric *settings); +static void account_widget_generic_int_changed_cb (GtkWidget *widget, + GossipAccountWidgetGeneric *settings); +static void account_widget_generic_checkbutton_toggled_cb (GtkWidget *widget, + GossipAccountWidgetGeneric *settings); +static gchar * account_widget_generic_format_param_name (const gchar *param_name); +static void account_widget_generic_setup_foreach (McProtocolParam *param, + GossipAccountWidgetGeneric *settings); +static void account_widget_generic_destroy_cb (GtkWidget *widget, + GossipAccountWidgetGeneric *settings); + +static gboolean +account_widget_generic_entry_focus_cb (GtkWidget *widget, + GdkEventFocus *event, + GossipAccountWidgetGeneric *settings) +{ + const gchar *str; + const gchar *param_name; + + str = gtk_entry_get_text (GTK_ENTRY (widget)); + param_name = g_object_get_data (G_OBJECT (widget), "param_name"); + + mc_account_set_param_string (settings->account, param_name, str); + + return FALSE; +} + +static void +account_widget_generic_int_changed_cb (GtkWidget *widget, + GossipAccountWidgetGeneric *settings) +{ + const gchar *param_name; + gint value; + + value = gtk_spin_button_get_value (GTK_SPIN_BUTTON (widget)); + param_name = g_object_get_data (G_OBJECT (widget), "param_name"); + + mc_account_set_param_int (settings->account, param_name, value); +} + +static void +account_widget_generic_checkbutton_toggled_cb (GtkWidget *widget, + GossipAccountWidgetGeneric *settings) +{ + gboolean active; + const gchar *param_name; + + active = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (widget)); + param_name = g_object_get_data (G_OBJECT (widget), "param_name"); + + mc_account_set_param_boolean (settings->account, param_name, active); +} + +static gchar * +account_widget_generic_format_param_name (const gchar *param_name) +{ + gchar *str; + gchar *p; + + str = g_strdup (param_name); + + if (str && g_ascii_isalpha (str[0])) { + str[0] = g_ascii_toupper (str[0]); + } + + while ((p = strchr (str, '-')) != NULL) { + if (p[1] != '\0' && g_ascii_isalpha (p[1])) { + p[0] = ' '; + p[1] = g_ascii_toupper (p[1]); + } + + p++; + } + + return str; +} + +static void +account_widget_generic_setup_foreach (McProtocolParam *param, + GossipAccountWidgetGeneric *settings) +{ + GtkWidget *widget; + gchar *param_name_formatted; + + param_name_formatted = account_widget_generic_format_param_name (param->name); + + gtk_table_resize (GTK_TABLE (settings->table_settings), + ++settings->n_rows, + 2); + + if (param->signature[0] == 's') { + gchar *str = NULL; + + str = g_strdup_printf (_("%s:"), param_name_formatted); + widget = gtk_label_new (str); + g_free (str); + + gtk_size_group_add_widget (settings->size_group, widget); + gtk_table_attach (GTK_TABLE (settings->table_settings), + widget, + 0, 1, + settings->n_rows - 1, settings->n_rows, + GTK_FILL, 0, + 0, 0); + + str = NULL; + widget = gtk_entry_new (); + mc_account_get_param_string (settings->account, + param->name, + &str); + if (str) { + gtk_entry_set_text (GTK_ENTRY (widget), str); + g_free (str); + } + + if (strstr (param->name, "password")) { + gtk_entry_set_visibility (GTK_ENTRY (widget), FALSE); + } + + g_signal_connect (widget, "focus-out-event", + G_CALLBACK (account_widget_generic_entry_focus_cb), + settings); + + gtk_table_attach (GTK_TABLE (settings->table_settings), + widget, + 1, 2, + settings->n_rows - 1, settings->n_rows, + GTK_FILL | GTK_EXPAND, 0, + 0, 0); + } + else if (param->signature[0] == 'q' || + param->signature[0] == 'n') { + gchar *str = NULL; + gint value = 0; + + str = g_strdup_printf (_("%s:"), param_name_formatted); + widget = gtk_label_new (str); + g_free (str); + + gtk_size_group_add_widget (settings->size_group, widget); + gtk_table_attach (GTK_TABLE (settings->table_settings), + widget, + 0, 1, + settings->n_rows - 1, settings->n_rows, + GTK_FILL, 0, + 0, 0); + + widget = gtk_spin_button_new_with_range (0, G_MAXINT, 1); + mc_account_get_param_int (settings->account, + param->name, + &value); + gtk_spin_button_set_value (GTK_SPIN_BUTTON (widget), value); + + g_signal_connect (widget, "value-changed", + G_CALLBACK (account_widget_generic_int_changed_cb), + settings); + + gtk_table_attach (GTK_TABLE (settings->table_settings), + widget, + 1, 2, + settings->n_rows - 1, settings->n_rows, + GTK_FILL | GTK_EXPAND, 0, + 0, 0); + } + else if (param->signature[0] == 'b') { + gboolean value; + + mc_account_get_param_boolean (settings->account, + param->name, + &value); + + widget = gtk_check_button_new_with_label (param_name_formatted); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget), value); + + g_signal_connect (widget, "toggled", + G_CALLBACK (account_widget_generic_checkbutton_toggled_cb), + settings); + + gtk_table_attach (GTK_TABLE (settings->table_settings), + widget, + 0, 2, + settings->n_rows - 1, settings->n_rows, + GTK_FILL | GTK_EXPAND, 0, + 0, 0); + } else { + g_assert_not_reached (); + } + + g_free (param_name_formatted); + + g_object_set_data_full (G_OBJECT (widget), "param_name", + g_strdup (param->name), g_free); +} + +static void +accounts_widget_generic_setup (GossipAccountWidgetGeneric *settings) +{ + McProtocol *protocol; + McProfile *profile; + GSList *params; + + profile = mc_account_get_profile (settings->account); + protocol = mc_profile_get_protocol (profile); + params = mc_protocol_get_params (protocol); + + g_slist_foreach (params, + (GFunc) account_widget_generic_setup_foreach, + settings); + + g_slist_free (params); +} + +static void +account_widget_generic_destroy_cb (GtkWidget *widget, + GossipAccountWidgetGeneric *settings) +{ + g_object_unref (settings->account); + g_object_unref (settings->size_group); + + g_free (settings); +} + +GtkWidget * +gossip_account_widget_generic_new (McAccount *account, + GtkWidget *label_name) +{ + GossipAccountWidgetGeneric *settings; + + g_return_val_if_fail (MC_IS_ACCOUNT (account), NULL); + g_return_val_if_fail (GTK_IS_WIDGET (label_name), NULL); + + settings = g_new0 (GossipAccountWidgetGeneric, 1); + + settings->account = g_object_ref (account); + + settings->table_settings = gtk_table_new (0, 2, FALSE); + gtk_table_set_row_spacings (GTK_TABLE (settings->table_settings), 6); + gtk_table_set_col_spacings (GTK_TABLE (settings->table_settings), 6); + settings->sw = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (settings->sw), + GTK_POLICY_AUTOMATIC, + GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_add_with_viewport (GTK_SCROLLED_WINDOW (settings->sw), + settings->table_settings); + + settings->size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + if (label_name) { + gtk_size_group_add_widget (settings->size_group, label_name); + } + + accounts_widget_generic_setup (settings); + + g_signal_connect (settings->sw, "destroy", + G_CALLBACK (account_widget_generic_destroy_cb), + settings); + + gtk_widget_show_all (settings->sw); + + return settings->sw; +} diff --git a/libempathy-gtk/gossip-account-widget-generic.h b/libempathy-gtk/gossip-account-widget-generic.h new file mode 100644 index 00000000..1a3db63a --- /dev/null +++ b/libempathy-gtk/gossip-account-widget-generic.h @@ -0,0 +1,38 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2006-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Xavier Claessens <xclaesse@gmail.com> + * Martyn Russell <martyn@imendio.com> + */ + +#ifndef __GOSSIP_ACCOUNT_WIDGET_GENERIC_H__ +#define __GOSSIP_ACCOUNT_WIDGET_GENERIC_H__ + +#include <gtk/gtk.h> + +#include <libmissioncontrol/mc-account.h> + +G_BEGIN_DECLS + +GtkWidget *gossip_account_widget_generic_new (McAccount *account, + GtkWidget *label_name); + +G_END_DECLS + +#endif /* __GOSSIP_ACCOUNT_WIDGET_GENERIC_H__ */ diff --git a/libempathy-gtk/gossip-accounts-dialog.c b/libempathy-gtk/gossip-accounts-dialog.c new file mode 100644 index 00000000..43d97477 --- /dev/null +++ b/libempathy-gtk/gossip-accounts-dialog.c @@ -0,0 +1,1032 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2005-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Martyn Russell <martyn@imendio.com> + * Xavier Claessens <xclaesse@gmail.com> + */ + +#include <config.h> + +#include <string.h> +#include <stdlib.h> + +#include <gtk/gtk.h> +#include <glade/glade.h> +#include <glib/gi18n.h> +#include <dbus/dbus-glib.h> + +#include <libmissioncontrol/mc-account.h> +#include <libmissioncontrol/mc-profile.h> +#include <libmissioncontrol/mission-control.h> +#include <libmissioncontrol/mc-account-monitor.h> +#include <libtelepathy/tp-constants.h> + +#include <libempathy/empathy-session.h> +#include <libempathy/gossip-debug.h> +#include <libempathy/gossip-paths.h> +#include <libempathy/gossip-utils.h> +#include <libempathy-gtk/gossip-ui-utils.h> + +#include "gossip-accounts-dialog.h" +#include "gossip-profile-chooser.h" +#include "gossip-account-widget-generic.h" + +#define DEBUG_DOMAIN "AccountDialog" + +/* Flashing delay for icons (milliseconds). */ +#define FLASH_TIMEOUT 500 + +typedef struct { + GtkWidget *window; + + GtkWidget *alignment_settings; + + GtkWidget *vbox_details; + GtkWidget *frame_no_account; + GtkWidget *label_no_account; + GtkWidget *label_no_account_blurb; + + GtkWidget *treeview; + + GtkWidget *button_remove; + GtkWidget *button_connect; + + GtkWidget *frame_new_account; + GtkWidget *combobox_profile; + GtkWidget *entry_name; + GtkWidget *table_new_account; + GtkWidget *button_create; + GtkWidget *button_cancel; + + GtkWidget *image_type; + GtkWidget *label_name; + GtkWidget *label_type; + GtkWidget *settings_widget; + + gboolean connecting_show; + guint connecting_id; + gboolean account_changed; +} GossipAccountsDialog; + +enum { + COL_NAME, + COL_STATUS, + COL_ACCOUNT_POINTER, + COL_COUNT +}; + +static void accounts_dialog_setup (GossipAccountsDialog *dialog); +static void accounts_dialog_update_account (GossipAccountsDialog *dialog, + McAccount *account); +static void accounts_dialog_model_setup (GossipAccountsDialog *dialog); +static void accounts_dialog_model_add_columns (GossipAccountsDialog *dialog); +static void accounts_dialog_model_select_first (GossipAccountsDialog *dialog); +static void accounts_dialog_model_pixbuf_data_func (GtkTreeViewColumn *tree_column, + GtkCellRenderer *cell, + GtkTreeModel *model, + GtkTreeIter *iter, + GossipAccountsDialog *dialog); +static McAccount *accounts_dialog_model_get_selected (GossipAccountsDialog *dialog); +static void accounts_dialog_model_set_selected (GossipAccountsDialog *dialog, + McAccount *account); +static gboolean accounts_dialog_model_remove_selected (GossipAccountsDialog *dialog); +static void accounts_dialog_model_selection_changed (GtkTreeSelection *selection, + GossipAccountsDialog *dialog); +static void accounts_dialog_add_account (GossipAccountsDialog *dialog, + McAccount *account); +static void accounts_dialog_account_added_cb (McAccountMonitor *monitor, + gchar *unique_name, + GossipAccountsDialog *dialog); +static void accounts_dialog_account_removed_cb (McAccountMonitor *monitor, + gchar *unique_name, + GossipAccountsDialog *dialog); +static gboolean accounts_dialog_row_changed_foreach (GtkTreeModel *model, + GtkTreePath *path, + GtkTreeIter *iter, + gpointer user_data); +static gboolean accounts_dialog_flash_connecting_cb (GossipAccountsDialog *dialog); +static void accounts_dialog_status_changed_cb (MissionControl *mc, + TelepathyConnectionStatus status, + McPresence presence, + TelepathyConnectionStatusReason reason, + const gchar *unique_name, + GossipAccountsDialog *dialog); +static void accounts_dialog_entry_name_changed_cb (GtkWidget *widget, + GossipAccountsDialog *dialog); +static void accounts_dialog_button_create_clicked_cb (GtkWidget *button, + GossipAccountsDialog *dialog); +static void accounts_dialog_button_cancel_clicked_cb (GtkWidget *button, + GossipAccountsDialog *dialog); +static void accounts_dialog_button_connect_clicked_cb (GtkWidget *button, + GossipAccountsDialog *dialog); +static void accounts_dialog_button_add_clicked_cb (GtkWidget *button, + GossipAccountsDialog *dialog); +static void accounts_dialog_remove_response_cb (GtkWidget *dialog, + gint response, + McAccount *account); +static void accounts_dialog_button_remove_clicked_cb (GtkWidget *button, + GossipAccountsDialog *dialog); +static void accounts_dialog_treeview_row_activated_cb (GtkTreeView *tree_view, + GtkTreePath *path, + GtkTreeViewColumn *column, + GossipAccountsDialog *dialog); +static void accounts_dialog_response_cb (GtkWidget *widget, + gint response, + GossipAccountsDialog *dialog); +static void accounts_dialog_destroy_cb (GtkWidget *widget, + GossipAccountsDialog *dialog); + +static void +accounts_dialog_setup (GossipAccountsDialog *dialog) +{ + GtkTreeView *view; + GtkListStore *store; + GtkTreeSelection *selection; + GtkTreeIter iter; + GList *accounts, *l; + MissionControl *mc; + + view = GTK_TREE_VIEW (dialog->treeview); + store = GTK_LIST_STORE (gtk_tree_view_get_model (view)); + selection = gtk_tree_view_get_selection (view); + + mc = empathy_session_get_mission_control (); + accounts = mc_accounts_list (); + + for (l = accounts; l; l = l->next) { + McAccount *account; + const gchar *name; + TelepathyConnectionStatus status; + + account = l->data; + + name = mc_account_get_display_name (account); + if (!name) { + continue; + } + + status = mission_control_get_connection_status (mc, account, NULL); + + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, + COL_NAME, name, + COL_STATUS, status, + COL_ACCOUNT_POINTER, account, + -1); + + accounts_dialog_status_changed_cb (mc, + status, + MC_PRESENCE_UNSET, + TP_CONN_STATUS_REASON_NONE_SPECIFIED, + mc_account_get_unique_name (account), + dialog); + + g_object_unref (account); + } + + g_list_free (accounts); +} + +static void +accounts_dialog_update_connect_button (GossipAccountsDialog *dialog) +{ + McAccount *account; + GtkWidget *image; + const gchar *stock_id; + const gchar *label; + + account = accounts_dialog_model_get_selected (dialog); + + if (!account) { + gtk_widget_set_sensitive (dialog->button_connect, FALSE); + return; + } + + if (mc_account_is_enabled (account)) { + label = _("Disable"); + stock_id = GTK_STOCK_DISCONNECT; + } else { + label = _("Enable"); + stock_id = GTK_STOCK_CONNECT; + } + + image = gtk_image_new_from_stock (stock_id, GTK_ICON_SIZE_BUTTON); + + gtk_button_set_label (GTK_BUTTON (dialog->button_connect), label); + gtk_button_set_image (GTK_BUTTON (dialog->button_connect), image); +} + +static void +accounts_dialog_update_account (GossipAccountsDialog *dialog, + McAccount *account) +{ + if (dialog->settings_widget) { + gtk_widget_destroy (dialog->settings_widget); + dialog->settings_widget = NULL; + } + + if (!account) { + GtkTreeView *view; + GtkTreeModel *model; + + gtk_widget_show (dialog->frame_no_account); + gtk_widget_hide (dialog->vbox_details); + + gtk_widget_set_sensitive (dialog->button_connect, FALSE); + gtk_widget_set_sensitive (dialog->button_remove, FALSE); + + view = GTK_TREE_VIEW (dialog->treeview); + model = gtk_tree_view_get_model (view); + + if (gtk_tree_model_iter_n_children (model, NULL) > 0) { + gtk_label_set_markup (GTK_LABEL (dialog->label_no_account), + _("<b>No Account Selected</b>")); + gtk_label_set_markup (GTK_LABEL (dialog->label_no_account_blurb), + _("To add a new account, you can click on the " + "'Add' button and a new entry will be created " + "for you to start configuring.\n" + "\n" + "If you do not want to add an account, simply " + "click on the account you want to configure in " + "the list on the left.")); + } else { + gtk_label_set_markup (GTK_LABEL (dialog->label_no_account), + _("<b>No Accounts Configured</b>")); + gtk_label_set_markup (GTK_LABEL (dialog->label_no_account_blurb), + _("To add a new account, you can click on the " + "'Add' button and a new entry will be created " + "for you to start configuring.")); + } + } else { + McProfile *profile; + const gchar *config_ui; + + gtk_widget_hide (dialog->frame_no_account); + gtk_widget_show (dialog->vbox_details); + + profile = mc_account_get_profile (account); + config_ui = mc_profile_get_configuration_ui (profile); + + if (strcmp (config_ui, "blah") == 0) { + } else { + dialog->settings_widget = + gossip_account_widget_generic_new (account, + dialog->label_name); + } + + gtk_widget_grab_focus (dialog->settings_widget); + } + + if (dialog->settings_widget) { + gtk_container_add (GTK_CONTAINER (dialog->alignment_settings), + dialog->settings_widget); + } + + if (account) { + McProfile *profile; + GdkPixbuf *pixbuf; + + pixbuf = gossip_pixbuf_from_account (account, GTK_ICON_SIZE_DIALOG); + gtk_image_set_from_pixbuf (GTK_IMAGE (dialog->image_type), pixbuf); + if (pixbuf) { + g_object_unref (pixbuf); + } + + profile = mc_account_get_profile (account); + + gtk_label_set_text (GTK_LABEL (dialog->label_type), + mc_profile_get_display_name (profile)); + gtk_label_set_text (GTK_LABEL (dialog->label_name), + mc_account_get_display_name (account)); + } +} + +static void +accounts_dialog_model_setup (GossipAccountsDialog *dialog) +{ + GtkListStore *store; + GtkTreeSelection *selection; + + store = gtk_list_store_new (COL_COUNT, + G_TYPE_STRING, /* name */ + G_TYPE_UINT, /* status */ + MC_TYPE_ACCOUNT); /* account */ + + gtk_tree_view_set_model (GTK_TREE_VIEW (dialog->treeview), + GTK_TREE_MODEL (store)); + + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (dialog->treeview)); + gtk_tree_selection_set_mode (selection, GTK_SELECTION_SINGLE); + + g_signal_connect (selection, "changed", + G_CALLBACK (accounts_dialog_model_selection_changed), + dialog); + + gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (store), + COL_NAME, GTK_SORT_ASCENDING); + + accounts_dialog_model_add_columns (dialog); + + g_object_unref (store); +} + +static void +accounts_dialog_model_add_columns (GossipAccountsDialog *dialog) +{ + GtkTreeView *view; + GtkTreeViewColumn *column; + GtkCellRenderer *cell; + + view = GTK_TREE_VIEW (dialog->treeview); + gtk_tree_view_set_headers_visible (view, TRUE); + + /* account name/status */ + column = gtk_tree_view_column_new (); + gtk_tree_view_column_set_title (column, _("Accounts")); + + cell = gtk_cell_renderer_pixbuf_new (); + gtk_tree_view_column_pack_start (column, cell, FALSE); + gtk_tree_view_column_set_cell_data_func (column, cell, + (GtkTreeCellDataFunc) + accounts_dialog_model_pixbuf_data_func, + dialog, + NULL); + + cell = gtk_cell_renderer_text_new (); + g_object_set (cell, "ellipsize", PANGO_ELLIPSIZE_END, NULL); + gtk_tree_view_column_pack_start (column, cell, TRUE); + gtk_tree_view_column_add_attribute (column, + cell, + "text", COL_NAME); + + gtk_tree_view_column_set_expand (column, TRUE); + gtk_tree_view_append_column (view, column); +} + +static void +accounts_dialog_model_select_first (GossipAccountsDialog *dialog) +{ + GtkTreeView *view; + GtkTreeModel *model; + GtkTreeSelection *selection; + GtkTreeIter iter; + + /* select first */ + view = GTK_TREE_VIEW (dialog->treeview); + model = gtk_tree_view_get_model (view); + + if (gtk_tree_model_get_iter_first (model, &iter)) { + selection = gtk_tree_view_get_selection (view); + gtk_tree_selection_select_iter (selection, &iter); + } else { + accounts_dialog_update_account (dialog, NULL); + } +} + +static void +accounts_dialog_model_pixbuf_data_func (GtkTreeViewColumn *tree_column, + GtkCellRenderer *cell, + GtkTreeModel *model, + GtkTreeIter *iter, + GossipAccountsDialog *dialog) +{ + McAccount *account; + GdkPixbuf *pixbuf; + TelepathyConnectionStatus status; + + gtk_tree_model_get (model, iter, + COL_STATUS, &status, + COL_ACCOUNT_POINTER, &account, + -1); + + pixbuf = gossip_pixbuf_from_account (account, GTK_ICON_SIZE_BUTTON); + + if (pixbuf) { + if (status == TP_CONN_STATUS_DISCONNECTED || + (status == TP_CONN_STATUS_CONNECTING && + !dialog->connecting_show)) { + GdkPixbuf *modded_pixbuf; + + modded_pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB, + TRUE, + 8, + gdk_pixbuf_get_width (pixbuf), + gdk_pixbuf_get_height (pixbuf)); + + gdk_pixbuf_saturate_and_pixelate (pixbuf, + modded_pixbuf, + 1.0, + TRUE); + g_object_unref (pixbuf); + pixbuf = modded_pixbuf; + } + } + + g_object_set (cell, + "visible", TRUE, + "pixbuf", pixbuf, + NULL); + + g_object_unref (account); + if (pixbuf) { + g_object_unref (pixbuf); + } +} + +static McAccount * +accounts_dialog_model_get_selected (GossipAccountsDialog *dialog) +{ + GtkTreeView *view; + GtkTreeModel *model; + GtkTreeSelection *selection; + GtkTreeIter iter; + McAccount *account; + + view = GTK_TREE_VIEW (dialog->treeview); + selection = gtk_tree_view_get_selection (view); + + if (!gtk_tree_selection_get_selected (selection, &model, &iter)) { + return NULL; + } + + gtk_tree_model_get (model, &iter, COL_ACCOUNT_POINTER, &account, -1); + + return account; +} + +static void +accounts_dialog_model_set_selected (GossipAccountsDialog *dialog, + McAccount *account) +{ + GtkTreeView *view; + GtkTreeSelection *selection; + GtkTreeModel *model; + GtkTreeIter iter; + gboolean ok; + + view = GTK_TREE_VIEW (dialog->treeview); + model = gtk_tree_view_get_model (view); + selection = gtk_tree_view_get_selection (view); + + for (ok = gtk_tree_model_get_iter_first (model, &iter); + ok; + ok = gtk_tree_model_iter_next (model, &iter)) { + McAccount *this_account; + gboolean equal; + + gtk_tree_model_get (model, &iter, + COL_ACCOUNT_POINTER, &this_account, + -1); + + equal = gossip_account_equal (this_account, account); + g_object_unref (this_account); + + if (equal) { + gtk_tree_selection_select_iter (selection, &iter); + break; + } + } +} + +static gboolean +accounts_dialog_model_remove_selected (GossipAccountsDialog *dialog) +{ + GtkTreeView *view; + GtkTreeModel *model; + GtkTreeSelection *selection; + GtkTreeIter iter; + + view = GTK_TREE_VIEW (dialog->treeview); + selection = gtk_tree_view_get_selection (view); + + if (!gtk_tree_selection_get_selected (selection, &model, &iter)) { + return FALSE; + } + + return gtk_list_store_remove (GTK_LIST_STORE (model), &iter); +} + +static void +accounts_dialog_model_selection_changed (GtkTreeSelection *selection, + GossipAccountsDialog *dialog) +{ + McAccount *account; + GtkTreeModel *model; + GtkTreeIter iter; + gboolean is_selection; + + is_selection = gtk_tree_selection_get_selected (selection, &model, &iter); + + gtk_widget_set_sensitive (dialog->button_remove, is_selection); + gtk_widget_set_sensitive (dialog->button_connect, is_selection); + + accounts_dialog_update_connect_button (dialog); + + account = accounts_dialog_model_get_selected (dialog); + accounts_dialog_update_account (dialog, account); + + if (account) { + g_object_unref (account); + } +} + +static void +accounts_dialog_add_account (GossipAccountsDialog *dialog, + McAccount *account) +{ + MissionControl *mc; + TelepathyConnectionStatus status; + const gchar *name; + GtkTreeView *view; + GtkTreeModel *model; + GtkListStore *store; + GtkTreeIter iter; + gboolean ok; + + view = GTK_TREE_VIEW (dialog->treeview); + model = gtk_tree_view_get_model (view); + store = GTK_LIST_STORE (model); + + for (ok = gtk_tree_model_get_iter_first (model, &iter); + ok; + ok = gtk_tree_model_iter_next (model, &iter)) { + McAccount *this_account; + gboolean equal; + + gtk_tree_model_get (model, &iter, + COL_ACCOUNT_POINTER, &this_account, + -1); + + equal = gossip_account_equal (this_account, account); + g_object_unref (this_account); + + if (equal) { + return; + } + } + + mc = empathy_session_get_mission_control (); + status = mission_control_get_connection_status (mc, account, NULL); + name = mc_account_get_display_name (account); + + g_return_if_fail (name != NULL); + + gossip_debug (DEBUG_DOMAIN, "Adding new account: %s", name); + + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, + COL_NAME, name, + COL_STATUS, status, + COL_ACCOUNT_POINTER, account, + -1); +} + +static void +accounts_dialog_account_added_cb (McAccountMonitor *monitor, + gchar *unique_name, + GossipAccountsDialog *dialog) +{ + McAccount *account; + + account = mc_account_lookup (unique_name); + accounts_dialog_add_account (dialog, account); + g_object_unref (account); +} + +static void +accounts_dialog_account_removed_cb (McAccountMonitor *monitor, + gchar *unique_name, + GossipAccountsDialog *dialog) +{ + MissionControl *mc; + McAccount *account; + + mc = empathy_session_get_mission_control (); + account = mc_account_lookup (unique_name); + + accounts_dialog_model_set_selected (dialog, account); + accounts_dialog_model_remove_selected (dialog); + + g_object_unref (account); +} + +static gboolean +accounts_dialog_row_changed_foreach (GtkTreeModel *model, + GtkTreePath *path, + GtkTreeIter *iter, + gpointer user_data) +{ + gtk_tree_model_row_changed (model, path, iter); + + return FALSE; +} + +static gboolean +accounts_dialog_flash_connecting_cb (GossipAccountsDialog *dialog) +{ + GtkTreeView *view; + GtkTreeModel *model; + + dialog->connecting_show = !dialog->connecting_show; + + view = GTK_TREE_VIEW (dialog->treeview); + model = gtk_tree_view_get_model (view); + + gtk_tree_model_foreach (model, accounts_dialog_row_changed_foreach, NULL); + + return TRUE; +} + +static void +accounts_dialog_status_changed_cb (MissionControl *mc, + TelepathyConnectionStatus status, + McPresence presence, + TelepathyConnectionStatusReason reason, + const gchar *unique_name, + GossipAccountsDialog *dialog) +{ + GtkTreeView *view; + GtkTreeSelection *selection; + GtkTreeModel *model; + GtkTreeIter iter; + gboolean ok; + McAccount *account; + GList *accounts, *l; + gboolean found = FALSE; + + /* Update the status in the model */ + view = GTK_TREE_VIEW (dialog->treeview); + selection = gtk_tree_view_get_selection (view); + model = gtk_tree_view_get_model (view); + account = mc_account_lookup (unique_name); + + for (ok = gtk_tree_model_get_iter_first (model, &iter); + ok; + ok = gtk_tree_model_iter_next (model, &iter)) { + McAccount *this_account; + gboolean equal; + + gtk_tree_model_get (model, &iter, + COL_ACCOUNT_POINTER, &this_account, + -1); + + equal = gossip_account_equal (this_account, account); + g_object_unref (this_account); + + if (equal) { + GtkTreePath *path; + + gtk_list_store_set (GTK_LIST_STORE (model), &iter, + COL_STATUS, status, + -1); + + path = gtk_tree_model_get_path (model, &iter); + gtk_tree_model_row_changed (model, path, &iter); + gtk_tree_path_free (path); + + break; + } + } + + g_object_unref (account); + + /* Start to flash account if status is connecting */ + if (status == TP_CONN_STATUS_CONNECTING) { + if (!dialog->connecting_id) { + dialog->connecting_id = g_timeout_add (FLASH_TIMEOUT, + (GSourceFunc) accounts_dialog_flash_connecting_cb, + dialog); + } + + return; + } + + /* Stop to flash if no account is connecting */ + accounts = mc_accounts_list (); + for (l = accounts; l; l = l->next) { + McAccount *this_account; + + this_account = l->data; + + if (mission_control_get_connection_status (mc, this_account, NULL) == TP_CONN_STATUS_CONNECTING) { + found = TRUE; + break; + } + + g_object_unref (this_account); + } + g_list_free (accounts); + + if (!found && dialog->connecting_id) { + g_source_remove (dialog->connecting_id); + dialog->connecting_id = 0; + } +} + +static void +accounts_dialog_entry_name_changed_cb (GtkWidget *widget, + GossipAccountsDialog *dialog) +{ + const gchar *str; + + str = gtk_entry_get_text (GTK_ENTRY (widget)); + gtk_widget_set_sensitive (dialog->button_create, !G_STR_EMPTY (str)); +} + +static void +accounts_dialog_button_create_clicked_cb (GtkWidget *button, + GossipAccountsDialog *dialog) +{ + McProfile *profile; + McAccount *account; + const gchar *str; + + /* Update widgets */ + gtk_widget_show (dialog->vbox_details); + gtk_widget_hide (dialog->frame_no_account); + gtk_widget_hide (dialog->frame_new_account); + + profile = gossip_profile_chooser_get_selected (dialog->combobox_profile); + + /* Create account */ + account = mc_account_create (profile); + + str = gtk_entry_get_text (GTK_ENTRY (dialog->entry_name)); + mc_account_set_display_name (account, str); + + accounts_dialog_add_account (dialog, account); + accounts_dialog_model_set_selected (dialog, account); + + g_object_unref (account); + g_object_unref (profile); +} + +static void +accounts_dialog_button_cancel_clicked_cb (GtkWidget *button, + GossipAccountsDialog *dialog) +{ + McAccount *account; + + gtk_widget_hide (dialog->vbox_details); + gtk_widget_hide (dialog->frame_no_account); + gtk_widget_hide (dialog->frame_new_account); + + account = accounts_dialog_model_get_selected (dialog); + accounts_dialog_update_account (dialog, account); +} + +static void +accounts_dialog_button_connect_clicked_cb (GtkWidget *button, + GossipAccountsDialog *dialog) +{ + McAccount *account; + gboolean enable; + + account = accounts_dialog_model_get_selected (dialog); + enable = (!mc_account_is_enabled (account)); + mc_account_set_enabled (account, enable); + accounts_dialog_update_connect_button (dialog); + + g_object_unref (account); +} + +static void +accounts_dialog_button_add_clicked_cb (GtkWidget *button, + GossipAccountsDialog *dialog) +{ + gtk_widget_hide (dialog->vbox_details); + gtk_widget_hide (dialog->frame_no_account); + gtk_widget_show (dialog->frame_new_account); + + gtk_combo_box_set_active (GTK_COMBO_BOX (dialog->combobox_profile), 0); + gtk_entry_set_text (GTK_ENTRY (dialog->entry_name), ""); + gtk_widget_grab_focus (dialog->entry_name); +} + +static void +accounts_dialog_remove_response_cb (GtkWidget *dialog, + gint response, + McAccount *account) +{ + if (response == GTK_RESPONSE_YES) { + mc_account_delete (account); + } + + gtk_widget_destroy (dialog); +} + +static void +accounts_dialog_button_remove_clicked_cb (GtkWidget *button, + GossipAccountsDialog *dialog) +{ + McAccount *account; + GtkWidget *message_dialog; + + account = accounts_dialog_model_get_selected (dialog); + + if (!mc_account_is_complete (account)) { + accounts_dialog_model_remove_selected (dialog); + return; + } + message_dialog = gtk_message_dialog_new + (GTK_WINDOW (dialog->window), + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_NONE, + _("You are about to remove your %s account!\n" + "Are you sure you want to proceed?"), + mc_account_get_display_name (account)); + + gtk_message_dialog_format_secondary_text + (GTK_MESSAGE_DIALOG (message_dialog), + _("Any associated conversations and chat rooms will NOT be " + "removed if you decide to proceed.\n" + "\n" + "Should you decide to add the account back at a later time, " + "they will still be available.")); + + gtk_dialog_add_button (GTK_DIALOG (message_dialog), + GTK_STOCK_CANCEL, + GTK_RESPONSE_NO); + gtk_dialog_add_button (GTK_DIALOG (message_dialog), + GTK_STOCK_REMOVE, + GTK_RESPONSE_YES); + + g_signal_connect (message_dialog, "response", + G_CALLBACK (accounts_dialog_remove_response_cb), + account); + + gtk_widget_show (message_dialog); +} + +static void +accounts_dialog_treeview_row_activated_cb (GtkTreeView *tree_view, + GtkTreePath *path, + GtkTreeViewColumn *column, + GossipAccountsDialog *dialog) +{ + + accounts_dialog_button_connect_clicked_cb (dialog->button_connect, + dialog); +} + +static void +accounts_dialog_response_cb (GtkWidget *widget, + gint response, + GossipAccountsDialog *dialog) +{ + gtk_widget_destroy (widget); +} + +static void +accounts_dialog_destroy_cb (GtkWidget *widget, + GossipAccountsDialog *dialog) +{ + MissionControl *mc; + McAccountMonitor *monitor; + GList *accounts, *l; + + mc = empathy_session_get_mission_control (); + monitor = mc_account_monitor_new (); + + /* Disconnect signals */ + g_signal_handlers_disconnect_by_func (monitor, + accounts_dialog_account_added_cb, + dialog); + g_signal_handlers_disconnect_by_func (monitor, + accounts_dialog_account_removed_cb, + dialog); + dbus_g_proxy_disconnect_signal (DBUS_G_PROXY (mc), "AccountStatusChanged", + G_CALLBACK (accounts_dialog_status_changed_cb), + dialog); + + /* Delete incomplete accounts */ + accounts = mc_accounts_list (); + for (l = accounts; l; l = l->next) { + McAccount *account; + + account = l->data; + if (!mc_account_is_complete (account)) { + /* FIXME: Warn the user the account is not complete + * and is going to be removed. */ + mc_account_delete (account); + } + + g_object_unref (account); + } + g_list_free (accounts); + + if (dialog->connecting_id) { + g_source_remove (dialog->connecting_id); + } + + g_free (dialog); +} + +GtkWidget * +gossip_accounts_dialog_show (void) +{ + static GossipAccountsDialog *dialog = NULL; + MissionControl *mc; + McAccountMonitor *monitor; + GladeXML *glade; + GtkWidget *bbox; + GtkWidget *button_close; + + if (dialog) { + gtk_window_present (GTK_WINDOW (dialog->window)); + return dialog->window; + } + + dialog = g_new0 (GossipAccountsDialog, 1); + + glade = gossip_glade_get_file ("empathy-accounts.glade", + "accounts_dialog", + NULL, + "accounts_dialog", &dialog->window, + "vbox_details", &dialog->vbox_details, + "frame_no_account", &dialog->frame_no_account, + "label_no_account", &dialog->label_no_account, + "label_no_account_blurb", &dialog->label_no_account_blurb, + "alignment_settings", &dialog->alignment_settings, + "dialog-action_area", &bbox, + "treeview", &dialog->treeview, + "frame_new_account", &dialog->frame_new_account, + "entry_name", &dialog->entry_name, + "table_new_account", &dialog->table_new_account, + "button_create", &dialog->button_create, + "button_cancel", &dialog->button_cancel, + "image_type", &dialog->image_type, + "label_type", &dialog->label_type, + "label_name", &dialog->label_name, + "button_remove", &dialog->button_remove, + "button_connect", &dialog->button_connect, + "button_close", &button_close, + NULL); + + gossip_glade_connect (glade, + dialog, + "accounts_dialog", "destroy", accounts_dialog_destroy_cb, + "accounts_dialog", "response", accounts_dialog_response_cb, + "button_create", "clicked", accounts_dialog_button_create_clicked_cb, + "button_cancel", "clicked", accounts_dialog_button_cancel_clicked_cb, + "entry_name", "changed", accounts_dialog_entry_name_changed_cb, + "treeview", "row-activated", accounts_dialog_treeview_row_activated_cb, + "button_connect", "clicked", accounts_dialog_button_connect_clicked_cb, + "button_add", "clicked", accounts_dialog_button_add_clicked_cb, + "button_remove", "clicked", accounts_dialog_button_remove_clicked_cb, + NULL); + + g_object_add_weak_pointer (G_OBJECT (dialog->window), (gpointer) &dialog); + + g_object_unref (glade); + + /* Create profile chooser */ + dialog->combobox_profile = gossip_profile_chooser_new (); + gtk_table_attach_defaults (GTK_TABLE (dialog->table_new_account), + dialog->combobox_profile, + 1, 2, + 0, 1); + gtk_widget_show (dialog->combobox_profile); + + /* Set up signalling */ + mc = empathy_session_get_mission_control (); + monitor = mc_account_monitor_new (); + + /* FIXME: connect account-enabled/disabled too */ + g_signal_connect (monitor, "account-created", + G_CALLBACK (accounts_dialog_account_added_cb), + dialog); + g_signal_connect (monitor, "account-deleted", + G_CALLBACK (accounts_dialog_account_removed_cb), + dialog); + dbus_g_proxy_connect_signal (DBUS_G_PROXY (mc), "AccountStatusChanged", + G_CALLBACK (accounts_dialog_status_changed_cb), + dialog, NULL); + + accounts_dialog_model_setup (dialog); + accounts_dialog_setup (dialog); + + gtk_widget_show (dialog->window); + + accounts_dialog_model_select_first (dialog); + + return dialog->window; +} + diff --git a/libempathy-gtk/gossip-accounts-dialog.h b/libempathy-gtk/gossip-accounts-dialog.h new file mode 100644 index 00000000..b6b0593c --- /dev/null +++ b/libempathy-gtk/gossip-accounts-dialog.h @@ -0,0 +1,34 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2005-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Martyn Russell <martyn@imendio.com> + */ + +#ifndef __GOSSIP_ACCOUNTS_DIALOG_H__ +#define __GOSSIP_ACCOUNTS_DIALOG_H__ + +#include <gtk/gtkwidget.h> + +G_BEGIN_DECLS + +GtkWidget *gossip_accounts_dialog_show (void); + +G_END_DECLS + +#endif /* __GOSSIP_ACCOUNTS_DIALOG_H__ */ diff --git a/libempathy-gtk/gossip-cell-renderer-expander.c b/libempathy-gtk/gossip-cell-renderer-expander.c new file mode 100644 index 00000000..e116ace7 --- /dev/null +++ b/libempathy-gtk/gossip-cell-renderer-expander.c @@ -0,0 +1,482 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2006-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Kristian Rietveld <kris@imendio.com> + */ + +/* To do: + * - should probably cancel animation if model changes + * - need to handle case where node-in-animation is removed + * - it only handles a single animation at a time; but I guess users + * aren't fast enough to trigger two or more animations at once anyway :P + * (could guard for this by just cancelling the "old" animation, and + * start the new one). + */ + +#include <gtk/gtktreeview.h> + +#include "gossip-cell-renderer-expander.h" + +#define GET_PRIV(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GOSSIP_TYPE_CELL_RENDERER_EXPANDER, GossipCellRendererExpanderPriv)) + +static void gossip_cell_renderer_expander_init (GossipCellRendererExpander *expander); +static void gossip_cell_renderer_expander_class_init (GossipCellRendererExpanderClass *klass); +static void gossip_cell_renderer_expander_get_property (GObject *object, + guint param_id, + GValue *value, + GParamSpec *pspec); +static void gossip_cell_renderer_expander_set_property (GObject *object, + guint param_id, + const GValue *value, + GParamSpec *pspec); +static void gossip_cell_renderer_expander_finalize (GObject *object); +static void gossip_cell_renderer_expander_get_size (GtkCellRenderer *cell, + GtkWidget *widget, + GdkRectangle *cell_area, + gint *x_offset, + gint *y_offset, + gint *width, + gint *height); +static void gossip_cell_renderer_expander_render (GtkCellRenderer *cell, + GdkWindow *window, + GtkWidget *widget, + GdkRectangle *background_area, + GdkRectangle *cell_area, + GdkRectangle *expose_area, + GtkCellRendererState flags); +static gboolean gossip_cell_renderer_expander_activate (GtkCellRenderer *cell, + GdkEvent *event, + GtkWidget *widget, + const gchar *path, + GdkRectangle *background_area, + GdkRectangle *cell_area, + GtkCellRendererState flags); + +enum { + PROP_0, + PROP_EXPANDER_STYLE, + PROP_EXPANDER_SIZE, + PROP_ACTIVATABLE +}; + +typedef struct _GossipCellRendererExpanderPriv GossipCellRendererExpanderPriv; + +struct _GossipCellRendererExpanderPriv { + GtkExpanderStyle expander_style; + gint expander_size; + + GtkTreeView *animation_view; + GtkTreeRowReference *animation_node; + GtkExpanderStyle animation_style; + guint animation_timeout; + GdkRectangle animation_area; + + guint activatable : 1; + guint animation_expanding : 1; +}; + +G_DEFINE_TYPE (GossipCellRendererExpander, gossip_cell_renderer_expander, GTK_TYPE_CELL_RENDERER) + +static void +gossip_cell_renderer_expander_init (GossipCellRendererExpander *expander) +{ + GossipCellRendererExpanderPriv *priv; + + priv = GET_PRIV (expander); + + priv->expander_style = GTK_EXPANDER_COLLAPSED; + priv->expander_size = 12; + priv->activatable = TRUE; + priv->animation_node = NULL; + + GTK_CELL_RENDERER (expander)->xpad = 2; + GTK_CELL_RENDERER (expander)->ypad = 2; + GTK_CELL_RENDERER (expander)->mode = GTK_CELL_RENDERER_MODE_ACTIVATABLE; +} + +static void +gossip_cell_renderer_expander_class_init (GossipCellRendererExpanderClass *klass) +{ + GObjectClass *object_class; + GtkCellRendererClass *cell_class; + + object_class = G_OBJECT_CLASS (klass); + cell_class = GTK_CELL_RENDERER_CLASS (klass); + + object_class->finalize = gossip_cell_renderer_expander_finalize; + + object_class->get_property = gossip_cell_renderer_expander_get_property; + object_class->set_property = gossip_cell_renderer_expander_set_property; + + cell_class->get_size = gossip_cell_renderer_expander_get_size; + cell_class->render = gossip_cell_renderer_expander_render; + cell_class->activate = gossip_cell_renderer_expander_activate; + + g_object_class_install_property (object_class, + PROP_EXPANDER_STYLE, + g_param_spec_enum ("expander-style", + "Expander Style", + "Style to use when painting the expander", + GTK_TYPE_EXPANDER_STYLE, + GTK_EXPANDER_COLLAPSED, + G_PARAM_READWRITE)); + + g_object_class_install_property (object_class, + PROP_EXPANDER_SIZE, + g_param_spec_int ("expander-size", + "Expander Size", + "The size of the expander", + 0, + G_MAXINT, + 12, + G_PARAM_READWRITE)); + + g_object_class_install_property (object_class, + PROP_ACTIVATABLE, + g_param_spec_boolean ("activatable", + "Activatable", + "The expander can be activated", + TRUE, + G_PARAM_READWRITE)); + + g_type_class_add_private (object_class, sizeof (GossipCellRendererExpanderPriv)); +} + +static void +gossip_cell_renderer_expander_get_property (GObject *object, + guint param_id, + GValue *value, + GParamSpec *pspec) +{ + GossipCellRendererExpander *expander; + GossipCellRendererExpanderPriv *priv; + + expander = GOSSIP_CELL_RENDERER_EXPANDER (object); + priv = GET_PRIV (expander); + + switch (param_id) { + case PROP_EXPANDER_STYLE: + g_value_set_enum (value, priv->expander_style); + break; + + case PROP_EXPANDER_SIZE: + g_value_set_int (value, priv->expander_size); + break; + + case PROP_ACTIVATABLE: + g_value_set_boolean (value, priv->activatable); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec); + break; + } +} + +static void +gossip_cell_renderer_expander_set_property (GObject *object, + guint param_id, + const GValue *value, + GParamSpec *pspec) +{ + GossipCellRendererExpander *expander; + GossipCellRendererExpanderPriv *priv; + + expander = GOSSIP_CELL_RENDERER_EXPANDER (object); + priv = GET_PRIV (expander); + + switch (param_id) { + case PROP_EXPANDER_STYLE: + priv->expander_style = g_value_get_enum (value); + break; + + case PROP_EXPANDER_SIZE: + priv->expander_size = g_value_get_int (value); + break; + + case PROP_ACTIVATABLE: + priv->activatable = g_value_get_boolean (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec); + break; + } +} + +static void +gossip_cell_renderer_expander_finalize (GObject *object) +{ + GossipCellRendererExpanderPriv *priv; + + priv = GET_PRIV (object); + + if (priv->animation_timeout) { + g_source_remove (priv->animation_timeout); + priv->animation_timeout = 0; + } + + if (priv->animation_node) { + gtk_tree_row_reference_free (priv->animation_node); + } + + (* G_OBJECT_CLASS (gossip_cell_renderer_expander_parent_class)->finalize) (object); +} + +GtkCellRenderer * +gossip_cell_renderer_expander_new (void) +{ + return g_object_new (GOSSIP_TYPE_CELL_RENDERER_EXPANDER, NULL); +} + +static void +gossip_cell_renderer_expander_get_size (GtkCellRenderer *cell, + GtkWidget *widget, + GdkRectangle *cell_area, + gint *x_offset, + gint *y_offset, + gint *width, + gint *height) +{ + GossipCellRendererExpander *expander; + GossipCellRendererExpanderPriv *priv; + + expander = (GossipCellRendererExpander*) cell; + priv = GET_PRIV (expander); + + if (cell_area) { + if (x_offset) { + *x_offset = cell->xalign * (cell_area->width - (priv->expander_size + (2 * cell->xpad))); + *x_offset = MAX (*x_offset, 0); + } + + if (y_offset) { + *y_offset = cell->yalign * (cell_area->height - (priv->expander_size + (2 * cell->ypad))); + *y_offset = MAX (*y_offset, 0); + } + } else { + if (x_offset) + *x_offset = 0; + + if (y_offset) + *y_offset = 0; + } + + if (width) + *width = cell->xpad * 2 + priv->expander_size; + + if (height) + *height = cell->ypad * 2 + priv->expander_size; +} + +static void +gossip_cell_renderer_expander_render (GtkCellRenderer *cell, + GdkWindow *window, + GtkWidget *widget, + GdkRectangle *background_area, + GdkRectangle *cell_area, + GdkRectangle *expose_area, + GtkCellRendererState flags) +{ + GossipCellRendererExpander *expander; + GossipCellRendererExpanderPriv *priv; + GtkExpanderStyle expander_style; + gint x_offset, y_offset; + + expander = (GossipCellRendererExpander*) cell; + priv = GET_PRIV (expander); + + if (priv->animation_node) { + GtkTreePath *path; + GdkRectangle rect; + + /* Not sure if I like this ... */ + path = gtk_tree_row_reference_get_path (priv->animation_node); + gtk_tree_view_get_background_area (priv->animation_view, path, + NULL, &rect); + gtk_tree_path_free (path); + + if (background_area->y == rect.y) + expander_style = priv->animation_style; + else + expander_style = priv->expander_style; + } else + expander_style = priv->expander_style; + + gossip_cell_renderer_expander_get_size (cell, widget, cell_area, + &x_offset, &y_offset, + NULL, NULL); + + gtk_paint_expander (widget->style, + window, + GTK_STATE_NORMAL, + expose_area, + widget, + "treeview", + cell_area->x + x_offset + cell->xpad + priv->expander_size / 2, + cell_area->y + y_offset + cell->ypad + priv->expander_size / 2, + expander_style); +} + +static void +invalidate_node (GtkTreeView *tree_view, + GtkTreePath *path) +{ + GdkWindow *bin_window; + GdkRectangle rect; + + bin_window = gtk_tree_view_get_bin_window (tree_view); + + gtk_tree_view_get_background_area (tree_view, path, NULL, &rect); + + rect.x = 0; + rect.width = GTK_WIDGET (tree_view)->allocation.width; + + gdk_window_invalidate_rect (bin_window, &rect, TRUE); +} + +static gboolean +do_animation (GossipCellRendererExpander *expander) +{ + GossipCellRendererExpanderPriv *priv; + GtkTreePath *path; + gboolean done = FALSE; + + priv = GET_PRIV (expander); + + if (priv->animation_expanding) { + if (priv->animation_style == GTK_EXPANDER_SEMI_COLLAPSED) + priv->animation_style = GTK_EXPANDER_SEMI_EXPANDED; + else if (priv->animation_style == GTK_EXPANDER_SEMI_EXPANDED) { + priv->animation_style = GTK_EXPANDER_EXPANDED; + done = TRUE; + } + } else { + if (priv->animation_style == GTK_EXPANDER_SEMI_EXPANDED) + priv->animation_style = GTK_EXPANDER_SEMI_COLLAPSED; + else if (priv->animation_style == GTK_EXPANDER_SEMI_COLLAPSED) { + priv->animation_style = GTK_EXPANDER_COLLAPSED; + done = TRUE; + } + } + + path = gtk_tree_row_reference_get_path (priv->animation_node); + invalidate_node (priv->animation_view, path); + gtk_tree_path_free (path); + + if (done) { + gtk_tree_row_reference_free (priv->animation_node); + priv->animation_node = NULL; + priv->animation_timeout = 0; + } + + return !done; +} + +static gboolean +animation_timeout (gpointer data) +{ + gboolean retval; + + GDK_THREADS_ENTER (); + + retval = do_animation (data); + + GDK_THREADS_LEAVE (); + + return retval; +} + +static void +gossip_cell_renderer_expander_start_animation (GossipCellRendererExpander *expander, + GtkTreeView *tree_view, + GtkTreePath *path, + gboolean expanding, + GdkRectangle *background_area) +{ + GossipCellRendererExpanderPriv *priv; + + priv = GET_PRIV (expander); + + if (expanding) { + priv->animation_style = GTK_EXPANDER_SEMI_COLLAPSED; + } else { + priv->animation_style = GTK_EXPANDER_SEMI_EXPANDED; + } + + invalidate_node (tree_view, path); + + priv->animation_expanding = expanding; + priv->animation_view = tree_view; + priv->animation_node = gtk_tree_row_reference_new (gtk_tree_view_get_model (tree_view), path); + priv->animation_timeout = g_timeout_add (50, animation_timeout, expander); +} + +static gboolean +gossip_cell_renderer_expander_activate (GtkCellRenderer *cell, + GdkEvent *event, + GtkWidget *widget, + const gchar *path_string, + GdkRectangle *background_area, + GdkRectangle *cell_area, + GtkCellRendererState flags) +{ + GossipCellRendererExpander *expander; + GossipCellRendererExpanderPriv *priv; + GtkTreePath *path; + gboolean animate; + gboolean expanding; + + expander = GOSSIP_CELL_RENDERER_EXPANDER (cell); + priv = GET_PRIV (cell); + + if (!GTK_IS_TREE_VIEW (widget) || !priv->activatable) + return FALSE; + + path = gtk_tree_path_new_from_string (path_string); + + if (gtk_tree_path_get_depth (path) > 1) { + gtk_tree_path_free (path); + return TRUE; + } + + g_object_get (gtk_widget_get_settings (GTK_WIDGET (widget)), + "gtk-enable-animations", &animate, + NULL); + + if (gtk_tree_view_row_expanded (GTK_TREE_VIEW (widget), path)) { + gtk_tree_view_collapse_row (GTK_TREE_VIEW (widget), path); + expanding = FALSE; + } else { + gtk_tree_view_expand_row (GTK_TREE_VIEW (widget), path, FALSE); + expanding = TRUE; + } + + if (animate) { + gossip_cell_renderer_expander_start_animation (expander, + GTK_TREE_VIEW (widget), + path, + expanding, + background_area); + } + + gtk_tree_path_free (path); + + return TRUE; +} diff --git a/libempathy-gtk/gossip-cell-renderer-expander.h b/libempathy-gtk/gossip-cell-renderer-expander.h new file mode 100644 index 00000000..7df47466 --- /dev/null +++ b/libempathy-gtk/gossip-cell-renderer-expander.h @@ -0,0 +1,59 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2006-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Kristian Rietveld <kris@imendio.com> + */ + +#ifndef __GOSSIP_CELL_RENDERER_EXPANDER_H__ +#define __GOSSIP_CELL_RENDERER_EXPANDER_H__ + +#include <gtk/gtkcellrenderer.h> + +G_BEGIN_DECLS + +#define GOSSIP_TYPE_CELL_RENDERER_EXPANDER (gossip_cell_renderer_expander_get_type ()) +#define GOSSIP_CELL_RENDERER_EXPANDER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GOSSIP_TYPE_CELL_RENDERER_EXPANDER, GossipCellRendererExpander)) +#define GOSSIP_CELL_RENDERER_EXPANDER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GOSSIP_TYPE_CELL_RENDERER_EXPANDER, GossipCellRendererExpanderClass)) +#define GOSSIP_IS_CELL_RENDERER_EXPANDER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GOSSIP_TYPE_CELL_RENDERER_EXPANDER)) +#define GOSSIP_IS_CELL_RENDERER_EXPANDER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GOSSIP_TYPE_CELL_RENDERER_EXPANDER)) +#define GOSSIP_CELL_RENDERER_EXPANDER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GOSSIP_TYPE_CELL_RENDERER_EXPANDER, GossipCellRendererExpanderClass)) + +typedef struct _GossipCellRendererExpander GossipCellRendererExpander; +typedef struct _GossipCellRendererExpanderClass GossipCellRendererExpanderClass; + +struct _GossipCellRendererExpander { + GtkCellRenderer parent; +}; + +struct _GossipCellRendererExpanderClass { + GtkCellRendererClass parent_class; + + /* Padding for future expansion */ + void (*_gtk_reserved1) (void); + void (*_gtk_reserved2) (void); + void (*_gtk_reserved3) (void); + void (*_gtk_reserved4) (void); +}; + +GType gossip_cell_renderer_expander_get_type (void) G_GNUC_CONST; +GtkCellRenderer *gossip_cell_renderer_expander_new (void); + +G_END_DECLS + +#endif /* __GOSSIP_CELL_RENDERER_EXPANDER_H__ */ diff --git a/libempathy-gtk/gossip-cell-renderer-text.c b/libempathy-gtk/gossip-cell-renderer-text.c new file mode 100644 index 00000000..2b54eedf --- /dev/null +++ b/libempathy-gtk/gossip-cell-renderer-text.c @@ -0,0 +1,368 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2004-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Mikael Hallendal <micke@imendio.com> + */ + +#include "config.h" + +#include <string.h> + +#include "gossip-cell-renderer-text.h" + +#define GET_PRIV(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GOSSIP_TYPE_CELL_RENDERER_TEXT, GossipCellRendererTextPriv)) + +struct _GossipCellRendererTextPriv { + gchar *name; + gchar *status; + gboolean is_group; + + gboolean is_valid; + gboolean is_selected; + + gboolean show_status; +}; + +static void gossip_cell_renderer_text_class_init (GossipCellRendererTextClass *klass); +static void gossip_cell_renderer_text_init (GossipCellRendererText *cell); +static void cell_renderer_text_finalize (GObject *object); +static void cell_renderer_text_get_property (GObject *object, + guint param_id, + GValue *value, + GParamSpec *pspec); +static void cell_renderer_text_set_property (GObject *object, + guint param_id, + const GValue *value, + GParamSpec *pspec); +static void cell_renderer_text_get_size (GtkCellRenderer *cell, + GtkWidget *widget, + GdkRectangle *cell_area, + gint *x_offset, + gint *y_offset, + gint *width, + gint *height); +static void cell_renderer_text_render (GtkCellRenderer *cell, + GdkDrawable *window, + GtkWidget *widget, + GdkRectangle *background_area, + GdkRectangle *cell_area, + GdkRectangle *expose_area, + GtkCellRendererState flags); +static void cell_renderer_text_update_text (GossipCellRendererText *cell, + GtkWidget *widget, + gboolean selected); + +/* Properties */ +enum { + PROP_0, + PROP_NAME, + PROP_STATUS, + PROP_IS_GROUP, + PROP_SHOW_STATUS, +}; + +G_DEFINE_TYPE (GossipCellRendererText, gossip_cell_renderer_text, GTK_TYPE_CELL_RENDERER_TEXT); + +static void +gossip_cell_renderer_text_class_init (GossipCellRendererTextClass *klass) +{ + GObjectClass *object_class; + GtkCellRendererClass *cell_class; + + object_class = G_OBJECT_CLASS (klass); + cell_class = GTK_CELL_RENDERER_CLASS (klass); + + object_class->finalize = cell_renderer_text_finalize; + + object_class->get_property = cell_renderer_text_get_property; + object_class->set_property = cell_renderer_text_set_property; + + cell_class->get_size = cell_renderer_text_get_size; + cell_class->render = cell_renderer_text_render; + + g_object_class_install_property (object_class, + PROP_NAME, + g_param_spec_string ("name", + "Name", + "Contact name", + NULL, + G_PARAM_READWRITE)); + g_object_class_install_property (object_class, + PROP_STATUS, + g_param_spec_string ("status", + "Status", + "Contact status string", + NULL, + G_PARAM_READWRITE)); + g_object_class_install_property (object_class, + PROP_IS_GROUP, + g_param_spec_boolean ("is_group", + "Is group", + "Whether this cell is a group", + FALSE, + G_PARAM_READWRITE)); + g_object_class_install_property (object_class, + PROP_SHOW_STATUS, + g_param_spec_boolean ("show-status", + "Show status", + "Whether to show the status line", + TRUE, + G_PARAM_READWRITE)); + + g_type_class_add_private (object_class, sizeof (GossipCellRendererTextPriv)); +} + +static void +gossip_cell_renderer_text_init (GossipCellRendererText *cell) +{ + GossipCellRendererTextPriv *priv; + + priv = GET_PRIV (cell); + + g_object_set (cell, + "ellipsize", PANGO_ELLIPSIZE_END, + NULL); + + priv->name = g_strdup (""); + priv->status = g_strdup (""); + priv->show_status = TRUE; +} + +static void +cell_renderer_text_finalize (GObject *object) +{ + GossipCellRendererText *cell; + GossipCellRendererTextPriv *priv; + + cell = GOSSIP_CELL_RENDERER_TEXT (object); + priv = GET_PRIV (cell); + + g_free (priv->name); + g_free (priv->status); + + (G_OBJECT_CLASS (gossip_cell_renderer_text_parent_class)->finalize) (object); +} + +static void +cell_renderer_text_get_property (GObject *object, + guint param_id, + GValue *value, + GParamSpec *pspec) +{ + GossipCellRendererText *cell; + GossipCellRendererTextPriv *priv; + + cell = GOSSIP_CELL_RENDERER_TEXT (object); + priv = GET_PRIV (cell); + + switch (param_id) { + case PROP_NAME: + g_value_set_string (value, priv->name); + break; + case PROP_STATUS: + g_value_set_string (value, priv->status); + break; + case PROP_IS_GROUP: + g_value_set_boolean (value, priv->is_group); + break; + case PROP_SHOW_STATUS: + g_value_set_boolean (value, priv->show_status); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec); + break; + } +} + +static void +cell_renderer_text_set_property (GObject *object, + guint param_id, + const GValue *value, + GParamSpec *pspec) +{ + GossipCellRendererText *cell; + GossipCellRendererTextPriv *priv; + const gchar *str; + + cell = GOSSIP_CELL_RENDERER_TEXT (object); + priv = GET_PRIV (cell); + + switch (param_id) { + case PROP_NAME: + g_free (priv->name); + str = g_value_get_string (value); + priv->name = g_strdup (str ? str : ""); + g_strdelimit (priv->name, "\n\r\t", ' '); + priv->is_valid = FALSE; + break; + case PROP_STATUS: + g_free (priv->status); + str = g_value_get_string (value); + priv->status = g_strdup (str ? str : ""); + g_strdelimit (priv->status, "\n\r\t", ' '); + priv->is_valid = FALSE; + break; + case PROP_IS_GROUP: + priv->is_group = g_value_get_boolean (value); + priv->is_valid = FALSE; + break; + case PROP_SHOW_STATUS: + priv->show_status = g_value_get_boolean (value); + priv->is_valid = FALSE; + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec); + break; + } +} + +static void +cell_renderer_text_get_size (GtkCellRenderer *cell, + GtkWidget *widget, + GdkRectangle *cell_area, + gint *x_offset, + gint *y_offset, + gint *width, + gint *height) +{ + GossipCellRendererText *celltext; + GossipCellRendererTextPriv *priv; + + celltext = GOSSIP_CELL_RENDERER_TEXT (cell); + priv = GET_PRIV (cell); + + /* Only update if not already valid so we get the right size. */ + cell_renderer_text_update_text (celltext, widget, priv->is_selected); + + (GTK_CELL_RENDERER_CLASS (gossip_cell_renderer_text_parent_class)->get_size) (cell, widget, + cell_area, + x_offset, y_offset, + width, height); +} + +static void +cell_renderer_text_render (GtkCellRenderer *cell, + GdkWindow *window, + GtkWidget *widget, + GdkRectangle *background_area, + GdkRectangle *cell_area, + GdkRectangle *expose_area, + GtkCellRendererState flags) +{ + GossipCellRendererText *celltext; + + celltext = GOSSIP_CELL_RENDERER_TEXT (cell); + + cell_renderer_text_update_text (celltext, + widget, + (flags & GTK_CELL_RENDERER_SELECTED)); + + (GTK_CELL_RENDERER_CLASS (gossip_cell_renderer_text_parent_class)->render) ( + cell, window, + widget, + background_area, + cell_area, + expose_area, flags); +} + +static void +cell_renderer_text_update_text (GossipCellRendererText *cell, + GtkWidget *widget, + gboolean selected) +{ + GossipCellRendererTextPriv *priv; + PangoAttrList *attr_list; + PangoAttribute *attr_color, *attr_style, *attr_size; + GtkStyle *style; + gchar *str; + + priv = GET_PRIV (cell); + + if (priv->is_valid && priv->is_selected == selected) { + return; + } + + if (priv->is_group) { + g_object_set (cell, + "visible", TRUE, + "weight", PANGO_WEIGHT_BOLD, + "text", priv->name, + "attributes", NULL, + "xpad", 1, + "ypad", 1, + NULL); + + priv->is_selected = selected; + priv->is_valid = TRUE; + return; + } + + style = gtk_widget_get_style (widget); + + attr_list = pango_attr_list_new (); + + attr_style = pango_attr_style_new (PANGO_STYLE_ITALIC); + attr_style->start_index = strlen (priv->name) + 1; + attr_style->end_index = -1; + pango_attr_list_insert (attr_list, attr_style); + + if (!selected) { + GdkColor color; + + color = style->text_aa[GTK_STATE_NORMAL]; + + attr_color = pango_attr_foreground_new (color.red, color.green, color.blue); + attr_color->start_index = attr_style->start_index; + attr_color->end_index = -1; + pango_attr_list_insert (attr_list, attr_color); + } + + attr_size = pango_attr_size_new (pango_font_description_get_size (style->font_desc) / 1.2); + + attr_size->start_index = attr_style->start_index; + attr_size->end_index = -1; + pango_attr_list_insert (attr_list, attr_size); + + if (!priv->status || !priv->status[0] || !priv->show_status) { + str = g_strdup (priv->name); + } else { + str = g_strdup_printf ("%s\n%s", priv->name, priv->status); + } + + g_object_set (cell, + "visible", TRUE, + "weight", PANGO_WEIGHT_NORMAL, + "text", str, + "attributes", attr_list, + "xpad", 0, + "ypad", 1, + NULL); + + g_free (str); + pango_attr_list_unref (attr_list); + + priv->is_selected = selected; + priv->is_valid = TRUE; +} + +GtkCellRenderer * +gossip_cell_renderer_text_new (void) +{ + return g_object_new (GOSSIP_TYPE_CELL_RENDERER_TEXT, NULL); +} diff --git a/libempathy-gtk/gossip-cell-renderer-text.h b/libempathy-gtk/gossip-cell-renderer-text.h new file mode 100644 index 00000000..9b5a413b --- /dev/null +++ b/libempathy-gtk/gossip-cell-renderer-text.h @@ -0,0 +1,56 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2004-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Mikael Hallendal <micke@imendio.com> + */ + +#ifndef __GOSSIP_CELL_RENDERER_TEXT_H__ +#define __GOSSIP_CELL_RENDERER_TEXT_H__ + +#include <gtk/gtkcellrenderertext.h> + +G_BEGIN_DECLS + +#define GOSSIP_TYPE_CELL_RENDERER_TEXT (gossip_cell_renderer_text_get_type ()) +#define GOSSIP_CELL_RENDERER_TEXT(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GOSSIP_TYPE_CELL_RENDERER_TEXT, GossipCellRendererText)) +#define GOSSIP_CELL_RENDERER_TEXT_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GOSSIP_TYPE_CELL_RENDERER_TEXT, GossipCellRendererTextClass)) +#define GOSSIP_IS_CELL_RENDERER_TEXT(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GOSSIP_TYPE_CELL_RENDERER_TEXT)) +#define GOSSIP_IS_CELL_RENDERER_TEXT_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GOSSIP_TYPE_CELL_RENDERER_TEXT)) +#define GOSSIP_CELL_RENDERER_TEXT_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GOSSIP_TYPE_CELL_RENDERER_TEXT, GossipCellRendererTextClass)) + +typedef struct _GossipCellRendererText GossipCellRendererText; +typedef struct _GossipCellRendererTextClass GossipCellRendererTextClass; +typedef struct _GossipCellRendererTextPriv GossipCellRendererTextPriv; + +struct _GossipCellRendererText { + GtkCellRendererText parent; + + GossipCellRendererTextPriv *priv; +}; + +struct _GossipCellRendererTextClass { + GtkCellRendererTextClass parent_class; +}; + +GType gossip_cell_renderer_text_get_type (void) G_GNUC_CONST; +GtkCellRenderer * gossip_cell_renderer_text_new (void); + +G_END_DECLS + +#endif /* __GOSSIP_CELL_RENDERER_TEXT_H__ */ diff --git a/libempathy-gtk/gossip-chat-manager.c b/libempathy-gtk/gossip-chat-manager.c new file mode 100644 index 00000000..86cd0ea3 --- /dev/null +++ b/libempathy-gtk/gossip-chat-manager.c @@ -0,0 +1,327 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2004-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Martyn Russell <martyn@imendio.com> + */ + +#include "config.h" + +#include <string.h> + +#include <glib/gi18n.h> + +#include <libgossip/gossip-account.h> +#include <libgossip/gossip-event.h> +#include <libgossip/gossip-session.h> +#include <libgossip/gossip-message.h> +#include <libgossip/gossip-debug.h> +#include <libgossip/gossip-event-manager.h> + +#include "gossip-app.h" +#include "gossip-chat.h" +#include "gossip-chat-manager.h" + +#define DEBUG_DOMAIN "ChatManager" + +#define GET_PRIV(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GOSSIP_TYPE_CHAT_MANAGER, GossipChatManagerPriv)) + +typedef struct _GossipChatManagerPriv GossipChatManagerPriv; + +struct _GossipChatManagerPriv { + GHashTable *chats; + GHashTable *events; +}; + +static void chat_manager_finalize (GObject *object); +static void chat_manager_new_message_cb (GossipSession *session, + GossipMessage *msg, + GossipChatManager *manager); +static void chat_manager_event_activated_cb (GossipEventManager *event_manager, + GossipEvent *event, + GObject *object); +static void chat_manager_get_chats_foreach (GossipContact *contact, + GossipPrivateChat *chat, + GList **chats); +static void chat_manager_chat_removed_cb (GossipChatManager *manager, + GossipChat *chat, + gboolean is_last_ref); + +G_DEFINE_TYPE (GossipChatManager, gossip_chat_manager, G_TYPE_OBJECT); + +static void +gossip_chat_manager_class_init (GossipChatManagerClass *class) +{ + GObjectClass *object_class; + + object_class = G_OBJECT_CLASS (class); + + object_class->finalize = chat_manager_finalize; + + g_type_class_add_private (object_class, sizeof (GossipChatManagerPriv)); +} + +static void +gossip_chat_manager_init (GossipChatManager *manager) +{ + GossipChatManagerPriv *priv; + + priv = GET_PRIV (manager); + + priv->chats = g_hash_table_new_full (gossip_contact_hash, + gossip_contact_equal, + (GDestroyNotify) g_object_unref, + (GDestroyNotify) g_object_unref); + + priv->events = g_hash_table_new_full (gossip_contact_hash, + gossip_contact_equal, + (GDestroyNotify) g_object_unref, + (GDestroyNotify) g_object_unref); + + /* Connect to signals on GossipSession to listen for new messages */ + g_signal_connect (gossip_app_get_session (), + "new-message", + G_CALLBACK (chat_manager_new_message_cb), + manager); +} + +static void +chat_manager_finalize (GObject *object) +{ + GossipChatManagerPriv *priv; + + priv = GET_PRIV (object); + + g_hash_table_destroy (priv->chats); + g_hash_table_destroy (priv->events); + + G_OBJECT_CLASS (gossip_chat_manager_parent_class)->finalize (object); +} + +static void +chat_manager_new_message_cb (GossipSession *session, + GossipMessage *message, + GossipChatManager *manager) +{ + GossipChatManagerPriv *priv; + GossipPrivateChat *chat; + GossipContact *sender; + GossipEvent *event = NULL; + GossipEvent *old_event; + + priv = GET_PRIV (manager); + + sender = gossip_message_get_sender (message); + chat = g_hash_table_lookup (priv->chats, sender); + + old_event = g_hash_table_lookup (priv->events, sender); + + /* Add event to event manager if one doesn't exist already. */ + if (!chat) { + gossip_debug (DEBUG_DOMAIN, "New chat for: %s", + gossip_contact_get_id (sender)); + chat = gossip_chat_manager_get_chat (manager, sender); + + if (!old_event) { + event = gossip_event_new (GOSSIP_EVENT_NEW_MESSAGE); + } + } else { + GossipChatWindow *window; + + window = gossip_chat_get_window (GOSSIP_CHAT (chat)); + + if (!window && !old_event) { + event = gossip_event_new (GOSSIP_EVENT_NEW_MESSAGE); + } + } + + gossip_private_chat_append_message (chat, message); + + if (event) { + gchar *str; + + str = g_strdup_printf (_("New message from %s"), + gossip_contact_get_name (sender)); + g_object_set (event, + "message", str, + "data", message, + NULL); + g_free (str); + + gossip_event_manager_add (gossip_app_get_event_manager (), + event, + chat_manager_event_activated_cb, + G_OBJECT (manager)); + + g_hash_table_insert (priv->events, + g_object_ref (sender), + g_object_ref (event)); + } +} + +static void +chat_manager_event_activated_cb (GossipEventManager *event_manager, + GossipEvent *event, + GObject *object) +{ + GossipMessage *message; + GossipContact *contact; + + message = GOSSIP_MESSAGE (gossip_event_get_data (event)); + contact = gossip_message_get_sender (message); + + gossip_chat_manager_show_chat (GOSSIP_CHAT_MANAGER (object), contact); +} + +static void +chat_manager_get_chats_foreach (GossipContact *contact, + GossipPrivateChat *chat, + GList **chats) +{ + const gchar *contact_id; + + contact_id = gossip_contact_get_id (contact); + *chats = g_list_prepend (*chats, g_strdup (contact_id)); +} + +GossipChatManager * +gossip_chat_manager_new (void) +{ + return g_object_new (GOSSIP_TYPE_CHAT_MANAGER, NULL); +} + +static void +chat_manager_chat_removed_cb (GossipChatManager *manager, + GossipChat *chat, + gboolean is_last_ref) +{ + GossipChatManagerPriv *priv; + GossipContact *contact; + + if (!is_last_ref) { + return; + } + + priv = GET_PRIV (manager); + + contact = gossip_chat_get_contact (chat); + + gossip_debug (DEBUG_DOMAIN, + "Removing an old chat:'%s'", + gossip_contact_get_id (contact)); + + g_hash_table_remove (priv->chats, contact); +} + +GossipPrivateChat * +gossip_chat_manager_get_chat (GossipChatManager *manager, + GossipContact *contact) +{ + GossipChatManagerPriv *priv; + GossipPrivateChat *chat; + + g_return_val_if_fail (GOSSIP_IS_CHAT_MANAGER (manager), NULL); + g_return_val_if_fail (GOSSIP_IS_CONTACT (contact), NULL); + + priv = GET_PRIV (manager); + + chat = g_hash_table_lookup (priv->chats, contact); + + if (!chat) { + GossipSession *session; + GossipAccount *account; + GossipContact *own_contact; + + session = gossip_app_get_session (); + account = gossip_contact_get_account (contact); + own_contact = gossip_session_get_own_contact (session, account); + + chat = gossip_private_chat_new (own_contact, contact); + g_hash_table_insert (priv->chats, + g_object_ref (contact), + chat); + g_object_add_toggle_ref (G_OBJECT (chat), + (GToggleNotify) chat_manager_chat_removed_cb, + manager); + + gossip_debug (DEBUG_DOMAIN, + "Creating a new chat:'%s'", + gossip_contact_get_id (contact)); + } + + return chat; +} + +GList * +gossip_chat_manager_get_chats (GossipChatManager *manager) +{ + GossipChatManagerPriv *priv; + GList *chats = NULL; + + g_return_val_if_fail (GOSSIP_IS_CHAT_MANAGER (manager), NULL); + + priv = GET_PRIV (manager); + + g_hash_table_foreach (priv->chats, + (GHFunc) chat_manager_get_chats_foreach, + &chats); + + chats = g_list_sort (chats, (GCompareFunc) strcmp); + + return chats; +} + +void +gossip_chat_manager_remove_events (GossipChatManager *manager, + GossipContact *contact) +{ + GossipChatManagerPriv *priv; + GossipEvent *event; + + g_return_if_fail (GOSSIP_IS_CHAT_MANAGER (manager)); + g_return_if_fail (GOSSIP_IS_CONTACT (contact)); + + gossip_debug (DEBUG_DOMAIN, + "Removing events for contact:'%s'", + gossip_contact_get_id (contact)); + + priv = GET_PRIV (manager); + + event = g_hash_table_lookup (priv->events, contact); + if (event) { + gossip_event_manager_remove (gossip_app_get_event_manager (), + event, G_OBJECT (manager)); + g_hash_table_remove (priv->events, contact); + } +} + +void +gossip_chat_manager_show_chat (GossipChatManager *manager, + GossipContact *contact) +{ + GossipPrivateChat *chat; + + g_return_if_fail (GOSSIP_IS_CHAT_MANAGER (manager)); + g_return_if_fail (GOSSIP_IS_CONTACT (contact)); + + chat = gossip_chat_manager_get_chat (manager, contact); + + gossip_chat_present (GOSSIP_CHAT (chat)); + + gossip_chat_manager_remove_events(manager, contact); +} diff --git a/libempathy-gtk/gossip-chat-manager.h b/libempathy-gtk/gossip-chat-manager.h new file mode 100644 index 00000000..7500b594 --- /dev/null +++ b/libempathy-gtk/gossip-chat-manager.h @@ -0,0 +1,65 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2004-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Martyn Russell <martyn@imendio.com> + */ + +#ifndef __GOSSIP_CHAT_MANAGER_H__ +#define __GOSSIP_CHAT_MANAGER_H__ + +#include <glib-object.h> + +#include <libempathy/gossip-contact.h> + +G_BEGIN_DECLS + +#define GOSSIP_TYPE_CHAT_MANAGER (gossip_chat_manager_get_type ()) +#define GOSSIP_CHAT_MANAGER(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GOSSIP_TYPE_CHAT_MANAGER, GossipChatManager)) +#define GOSSIP_CHAT_MANAGER_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), GOSSIP_TYPE_CHAT_MANAGER, GossipChatManagerClass)) +#define GOSSIP_IS_CHAT_MANAGER(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GOSSIP_TYPE_CHAT_MANAGER)) +#define GOSSIP_IS_CHAT_MANAGER_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GOSSIP_TYPE_CHAT_MANAGER)) +#define GOSSIP_CHAT_MANAGER_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GOSSIP_TYPE_CHAT_MANAGER, GossipChatManagerClass)) + +typedef struct _GossipChatManager GossipChatManager; +typedef struct _GossipChatManagerClass GossipChatManagerClass; + +#include "gossip-private-chat.h" + +struct _GossipChatManager { + GObject parent; +}; + +struct _GossipChatManagerClass { + GObjectClass parent_class; +}; + +GType gossip_chat_manager_get_type (void) G_GNUC_CONST; +GossipChatManager *gossip_chat_manager_new (void); +GossipPrivateChat *gossip_chat_manager_get_chat (GossipChatManager *manager, + GossipContact *contact); +GList * gossip_chat_manager_get_chats (GossipChatManager *manager); +void gossip_chat_manager_remove_events (GossipChatManager *manager, + GossipContact *contact); +void gossip_chat_manager_show_chat (GossipChatManager *manager, + GossipContact *contact); + +G_END_DECLS + +#endif /* __GOSSIP_CHAT_MANAGER_H__ */ + diff --git a/libempathy-gtk/gossip-chat-manager.loT b/libempathy-gtk/gossip-chat-manager.loT new file mode 100644 index 00000000..3d0d3ab0 --- /dev/null +++ b/libempathy-gtk/gossip-chat-manager.loT @@ -0,0 +1,7 @@ +# gossip-chat-manager.lo - a libtool object file +# Generated by ltmain.sh - GNU libtool 1.5.22 Debian 1.5.22-4 (1.1220.2.365 2005/12/18 22:14:06) +# +# Please DO NOT delete this file! +# It is necessary for linking the library. + +# Name of the PIC object. diff --git a/libempathy-gtk/gossip-chat-view.c b/libempathy-gtk/gossip-chat-view.c new file mode 100644 index 00000000..cc6482ee --- /dev/null +++ b/libempathy-gtk/gossip-chat-view.c @@ -0,0 +1,2151 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2002-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Mikael Hallendal <micke@imendio.com> + * Richard Hult <richard@imendio.com> + * Martyn Russell <martyn@imendio.com> + */ + +#include "config.h" + +#include <sys/types.h> +#include <string.h> +#include <time.h> + +#include <glib/gi18n.h> +#include <gtk/gtkbutton.h> +#include <gtk/gtkimage.h> +#include <gtk/gtkmenu.h> +#include <gtk/gtkmenuitem.h> +#include <gtk/gtkimagemenuitem.h> +#include <gtk/gtkstock.h> +#include <gtk/gtkscrolledwindow.h> +#include <gtk/gtksizegroup.h> +#include <glade/glade.h> + +#include <libmissioncontrol/mc-account.h> + +#include <libempathy/gossip-utils.h> +#include <libempathy/gossip-debug.h> +#include <libempathy/gossip-conf.h> + +#include "gossip-chat-view.h" +#include "gossip-chat.h" +#include "gossip-preferences.h" +#include "gossip-theme-manager.h" +#include "gossip-ui-utils.h" + +#define DEBUG_DOMAIN "ChatView" + +/* Number of seconds between timestamps when using normal mode, 5 minutes. */ +#define TIMESTAMP_INTERVAL 300 + +#define MAX_LINES 800 + +#define GET_PRIV(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GOSSIP_TYPE_CHAT_VIEW, GossipChatViewPriv)) + +typedef enum { + BLOCK_TYPE_NONE, + BLOCK_TYPE_SELF, + BLOCK_TYPE_OTHER, + BLOCK_TYPE_EVENT, + BLOCK_TYPE_TIME, + BLOCK_TYPE_INVITE +} BlockType; + +struct _GossipChatViewPriv { + GtkTextBuffer *buffer; + + gboolean irc_style; + time_t last_timestamp; + BlockType last_block_type; + + gboolean allow_scrolling; + gboolean is_group_chat; + + GtkTextMark *find_mark_previous; + GtkTextMark *find_mark_next; + gboolean find_wrapped; + gboolean find_last_direction; + + /* This is for the group chat so we know if the "other" last contact + * changed, so we know whether to insert a header or not. + */ + GossipContact *last_contact; + + guint notify_system_fonts_id; + guint notify_show_avatars_id; +}; + +typedef struct { + GossipSmiley smiley; + const gchar *pattern; +} GossipSmileyPattern; + +static const GossipSmileyPattern smileys[] = { + /* Forward smileys. */ + { GOSSIP_SMILEY_NORMAL, ":)" }, + { GOSSIP_SMILEY_WINK, ";)" }, + { GOSSIP_SMILEY_WINK, ";-)" }, + { GOSSIP_SMILEY_BIGEYE, "=)" }, + { GOSSIP_SMILEY_NOSE, ":-)" }, + { GOSSIP_SMILEY_CRY, ":'(" }, + { GOSSIP_SMILEY_SAD, ":(" }, + { GOSSIP_SMILEY_SAD, ":-(" }, + { GOSSIP_SMILEY_SCEPTICAL, ":/" }, + { GOSSIP_SMILEY_SCEPTICAL, ":\\" }, + { GOSSIP_SMILEY_BIGSMILE, ":D" }, + { GOSSIP_SMILEY_BIGSMILE, ":-D" }, + { GOSSIP_SMILEY_INDIFFERENT, ":|" }, + { GOSSIP_SMILEY_TOUNGE, ":p" }, + { GOSSIP_SMILEY_TOUNGE, ":-p" }, + { GOSSIP_SMILEY_TOUNGE, ":P" }, + { GOSSIP_SMILEY_TOUNGE, ":-P" }, + { GOSSIP_SMILEY_TOUNGE, ";p" }, + { GOSSIP_SMILEY_TOUNGE, ";-p" }, + { GOSSIP_SMILEY_TOUNGE, ";P" }, + { GOSSIP_SMILEY_TOUNGE, ";-P" }, + { GOSSIP_SMILEY_SHOCKED, ":o" }, + { GOSSIP_SMILEY_SHOCKED, ":-o" }, + { GOSSIP_SMILEY_SHOCKED, ":O" }, + { GOSSIP_SMILEY_SHOCKED, ":-O" }, + { GOSSIP_SMILEY_COOL, "8)" }, + { GOSSIP_SMILEY_COOL, "B)" }, + { GOSSIP_SMILEY_SORRY, "*|" }, + { GOSSIP_SMILEY_KISS, ":*" }, + { GOSSIP_SMILEY_SHUTUP, ":#" }, + { GOSSIP_SMILEY_SHUTUP, ":-#" }, + { GOSSIP_SMILEY_YAWN, "|O" }, + { GOSSIP_SMILEY_CONFUSED, ":S" }, + { GOSSIP_SMILEY_CONFUSED, ":s" }, + { GOSSIP_SMILEY_ANGEL, "<)" }, + { GOSSIP_SMILEY_OOOH, ":x" }, + { GOSSIP_SMILEY_LOOKAWAY, "*)" }, + { GOSSIP_SMILEY_LOOKAWAY, "*-)" }, + { GOSSIP_SMILEY_BLUSH, "*S" }, + { GOSSIP_SMILEY_BLUSH, "*s" }, + { GOSSIP_SMILEY_BLUSH, "*$" }, + { GOSSIP_SMILEY_COOLBIGSMILE, "8D" }, + { GOSSIP_SMILEY_ANGRY, ":@" }, + { GOSSIP_SMILEY_BOSS, "@)" }, + { GOSSIP_SMILEY_MONKEY, "#)" }, + { GOSSIP_SMILEY_SILLY, "O)" }, + { GOSSIP_SMILEY_SICK, "+o(" }, + + /* Backward smileys. */ + { GOSSIP_SMILEY_NORMAL, "(:" }, + { GOSSIP_SMILEY_WINK, "(;" }, + { GOSSIP_SMILEY_WINK, "(-;" }, + { GOSSIP_SMILEY_BIGEYE, "(=" }, + { GOSSIP_SMILEY_NOSE, "(-:" }, + { GOSSIP_SMILEY_CRY, ")':" }, + { GOSSIP_SMILEY_SAD, "):" }, + { GOSSIP_SMILEY_SAD, ")-:" }, + { GOSSIP_SMILEY_SCEPTICAL, "/:" }, + { GOSSIP_SMILEY_SCEPTICAL, "//:" }, + { GOSSIP_SMILEY_INDIFFERENT, "|:" }, + { GOSSIP_SMILEY_TOUNGE, "d:" }, + { GOSSIP_SMILEY_TOUNGE, "d-:" }, + { GOSSIP_SMILEY_TOUNGE, "d;" }, + { GOSSIP_SMILEY_TOUNGE, "d-;" }, + { GOSSIP_SMILEY_SHOCKED, "o:" }, + { GOSSIP_SMILEY_SHOCKED, "O:" }, + { GOSSIP_SMILEY_COOL, "(8" }, + { GOSSIP_SMILEY_COOL, "(B" }, + { GOSSIP_SMILEY_SORRY, "|*" }, + { GOSSIP_SMILEY_KISS, "*:" }, + { GOSSIP_SMILEY_SHUTUP, "#:" }, + { GOSSIP_SMILEY_SHUTUP, "#-:" }, + { GOSSIP_SMILEY_YAWN, "O|" }, + { GOSSIP_SMILEY_CONFUSED, "S:" }, + { GOSSIP_SMILEY_CONFUSED, "s:" }, + { GOSSIP_SMILEY_ANGEL, "(>" }, + { GOSSIP_SMILEY_OOOH, "x:" }, + { GOSSIP_SMILEY_LOOKAWAY, "(*" }, + { GOSSIP_SMILEY_LOOKAWAY, "(-*" }, + { GOSSIP_SMILEY_BLUSH, "S*" }, + { GOSSIP_SMILEY_BLUSH, "s*" }, + { GOSSIP_SMILEY_BLUSH, "$*" }, + { GOSSIP_SMILEY_ANGRY, "@:" }, + { GOSSIP_SMILEY_BOSS, "(@" }, + { GOSSIP_SMILEY_MONKEY, "#)" }, + { GOSSIP_SMILEY_SILLY, "(O" }, + { GOSSIP_SMILEY_SICK, ")o+" } +}; + +static void gossip_chat_view_class_init (GossipChatViewClass *klass); +static void gossip_chat_view_init (GossipChatView *view); +static void chat_view_finalize (GObject *object); +static gboolean chat_view_drag_motion (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + guint time); +static void chat_view_size_allocate (GtkWidget *widget, + GtkAllocation *alloc); +static void chat_view_setup_tags (GossipChatView *view); +static void chat_view_system_font_update (GossipChatView *view); +static void chat_view_notify_system_font_cb (GossipConf *conf, + const gchar *key, + gpointer user_data); +static void chat_view_notify_show_avatars_cb (GossipConf *conf, + const gchar *key, + gpointer user_data); +static void chat_view_populate_popup (GossipChatView *view, + GtkMenu *menu, + gpointer user_data); +static gboolean chat_view_event_cb (GossipChatView *view, + GdkEventMotion *event, + GtkTextTag *tag); +static gboolean chat_view_url_event_cb (GtkTextTag *tag, + GObject *object, + GdkEvent *event, + GtkTextIter *iter, + GtkTextBuffer *buffer); +static void chat_view_open_address_cb (GtkMenuItem *menuitem, + const gchar *url); +static void chat_view_copy_address_cb (GtkMenuItem *menuitem, + const gchar *url); +static void chat_view_clear_view_cb (GtkMenuItem *menuitem, + GossipChatView *view); +static void chat_view_insert_text_with_emoticons (GtkTextBuffer *buf, + GtkTextIter *iter, + const gchar *str); +static gboolean chat_view_is_scrolled_down (GossipChatView *view); +static void chat_view_theme_changed_cb (GossipThemeManager *manager, + GossipChatView *view); +static void chat_view_maybe_append_date_and_time (GossipChatView *view, + GossipMessage *msg); +static void chat_view_append_spacing (GossipChatView *view); +static void chat_view_append_text (GossipChatView *view, + const gchar *body, + const gchar *tag); +static void chat_view_maybe_append_fancy_header (GossipChatView *view, + GossipMessage *msg); +static void chat_view_append_irc_action (GossipChatView *view, + GossipMessage *msg); +static void chat_view_append_fancy_action (GossipChatView *view, + GossipMessage *msg); +static void chat_view_append_irc_message (GossipChatView *view, + GossipMessage *msg); +static void chat_view_append_fancy_message (GossipChatView *view, + GossipMessage *msg); + +G_DEFINE_TYPE (GossipChatView, gossip_chat_view, GTK_TYPE_TEXT_VIEW); + +static void +gossip_chat_view_class_init (GossipChatViewClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = chat_view_finalize; + widget_class->size_allocate = chat_view_size_allocate; + widget_class->drag_motion = chat_view_drag_motion; + + g_type_class_add_private (object_class, sizeof (GossipChatViewPriv)); +} + +static void +gossip_chat_view_init (GossipChatView *view) +{ + GossipChatViewPriv *priv; + gboolean show_avatars; + + priv = GET_PRIV (view); + + priv->buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)); + + priv->last_block_type = BLOCK_TYPE_NONE; + priv->last_timestamp = 0; + + priv->allow_scrolling = TRUE; + + priv->is_group_chat = FALSE; + + g_object_set (view, + "wrap-mode", GTK_WRAP_WORD_CHAR, + "editable", FALSE, + "cursor-visible", FALSE, + NULL); + + priv->notify_system_fonts_id = + gossip_conf_notify_add (gossip_conf_get (), + "/desktop/gnome/interface/document_font_name", + chat_view_notify_system_font_cb, + view); + chat_view_system_font_update (view); + + priv->notify_show_avatars_id = + gossip_conf_notify_add (gossip_conf_get (), + GOSSIP_PREFS_UI_SHOW_AVATARS, + chat_view_notify_show_avatars_cb, + view); + + chat_view_setup_tags (view); + + gossip_theme_manager_apply_saved (gossip_theme_manager_get (), view); + + show_avatars = FALSE; + gossip_conf_get_bool (gossip_conf_get (), + GOSSIP_PREFS_UI_SHOW_AVATARS, + &show_avatars); + + gossip_theme_manager_update_show_avatars (gossip_theme_manager_get (), + view, show_avatars); + + g_signal_connect (view, + "populate-popup", + G_CALLBACK (chat_view_populate_popup), + NULL); + + g_signal_connect_object (gossip_theme_manager_get (), + "theme-changed", + G_CALLBACK (chat_view_theme_changed_cb), + view, + 0); +} + +static void +chat_view_finalize (GObject *object) +{ + GossipChatView *view; + GossipChatViewPriv *priv; + + view = GOSSIP_CHAT_VIEW (object); + priv = GET_PRIV (view); + + gossip_debug (DEBUG_DOMAIN, "finalize: %p", object); + + gossip_conf_notify_remove (gossip_conf_get (), priv->notify_system_fonts_id); + gossip_conf_notify_remove (gossip_conf_get (), priv->notify_show_avatars_id); + + if (priv->last_contact) { + g_object_unref (priv->last_contact); + } + + G_OBJECT_CLASS (gossip_chat_view_parent_class)->finalize (object); +} + +static gboolean +chat_view_drag_motion (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + guint time) +{ + /* Don't handle drag motion, since we don't want the view to scroll as + * the result of dragging something across it. + */ + + return FALSE; +} + +static void +chat_view_size_allocate (GtkWidget *widget, + GtkAllocation *alloc) +{ + gboolean down; + + down = chat_view_is_scrolled_down (GOSSIP_CHAT_VIEW (widget)); + + GTK_WIDGET_CLASS (gossip_chat_view_parent_class)->size_allocate (widget, alloc); + + if (down) { + gossip_chat_view_scroll_down (GOSSIP_CHAT_VIEW (widget)); + } +} + +static void +chat_view_setup_tags (GossipChatView *view) +{ + GossipChatViewPriv *priv; + GtkTextTag *tag; + + priv = GET_PRIV (view); + + gtk_text_buffer_create_tag (priv->buffer, + "cut", + NULL); + + /* FIXME: Move to the theme and come up with something that looks a bit + * nicer. + */ + gtk_text_buffer_create_tag (priv->buffer, + "highlight", + "background", "yellow", + NULL); + + tag = gtk_text_buffer_create_tag (priv->buffer, + "link", + NULL); + + g_signal_connect (tag, + "event", + G_CALLBACK (chat_view_url_event_cb), + priv->buffer); + + g_signal_connect (view, + "motion-notify-event", + G_CALLBACK (chat_view_event_cb), + tag); +} + +static void +chat_view_system_font_update (GossipChatView *view) +{ + PangoFontDescription *font_description = NULL; + gchar *font_name; + + if (gossip_conf_get_string (gossip_conf_get (), + "/desktop/gnome/interface/document_font_name", + &font_name) && font_name) { + font_description = pango_font_description_from_string (font_name); + g_free (font_name); + } else { + font_description = NULL; + } + + gtk_widget_modify_font (GTK_WIDGET (view), font_description); + + if (font_description) { + pango_font_description_free (font_description); + } +} + +static void +chat_view_notify_system_font_cb (GossipConf *conf, + const gchar *key, + gpointer user_data) +{ + GossipChatView *view; + gboolean show_avatars = FALSE; + + view = user_data; + + chat_view_system_font_update (view); + + /* Ugly, again, to adjust the vertical position of the nick... Will fix + * this when reworking the theme manager so that view register + * themselves with it instead of the other way around. + */ + gossip_conf_get_bool (conf, + GOSSIP_PREFS_UI_SHOW_AVATARS, + &show_avatars); + + gossip_theme_manager_update_show_avatars (gossip_theme_manager_get (), + view, show_avatars); +} + +static void +chat_view_notify_show_avatars_cb (GossipConf *conf, + const gchar *key, + gpointer user_data) +{ + GossipChatView *view; + GossipChatViewPriv *priv; + gboolean show_avatars = FALSE; + + view = user_data; + priv = GET_PRIV (view); + + gossip_conf_get_bool (conf, key, &show_avatars); + + gossip_theme_manager_update_show_avatars (gossip_theme_manager_get (), + view, show_avatars); +} + +static void +chat_view_populate_popup (GossipChatView *view, + GtkMenu *menu, + gpointer user_data) +{ + GossipChatViewPriv *priv; + GtkTextTagTable *table; + GtkTextTag *tag; + gint x, y; + GtkTextIter iter, start, end; + GtkWidget *item; + gchar *str = NULL; + + priv = GET_PRIV (view); + + /* Clear menu item */ + if (gtk_text_buffer_get_char_count (priv->buffer) > 0) { + item = gtk_menu_item_new (); + gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item); + gtk_widget_show (item); + + item = gtk_image_menu_item_new_from_stock (GTK_STOCK_CLEAR, NULL); + gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item); + gtk_widget_show (item); + + g_signal_connect (item, + "activate", + G_CALLBACK (chat_view_clear_view_cb), + view); + } + + /* Link context menu items */ + table = gtk_text_buffer_get_tag_table (priv->buffer); + tag = gtk_text_tag_table_lookup (table, "link"); + + gtk_widget_get_pointer (GTK_WIDGET (view), &x, &y); + + gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (view), + GTK_TEXT_WINDOW_WIDGET, + x, y, + &x, &y); + + gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (view), &iter, x, y); + + start = end = iter; + + if (gtk_text_iter_backward_to_tag_toggle (&start, tag) && + gtk_text_iter_forward_to_tag_toggle (&end, tag)) { + str = gtk_text_buffer_get_text (priv->buffer, + &start, &end, FALSE); + } + + if (G_STR_EMPTY (str)) { + g_free (str); + return; + } + + /* NOTE: Set data just to get the string freed when not needed. */ + g_object_set_data_full (G_OBJECT (menu), + "url", str, + (GDestroyNotify) g_free); + + item = gtk_menu_item_new (); + gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item); + gtk_widget_show (item); + + item = gtk_menu_item_new_with_mnemonic (_("_Copy Link Address")); + g_signal_connect (item, + "activate", + G_CALLBACK (chat_view_copy_address_cb), + str); + gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item); + gtk_widget_show (item); + + item = gtk_menu_item_new_with_mnemonic (_("_Open Link")); + g_signal_connect (item, + "activate", + G_CALLBACK (chat_view_open_address_cb), + str); + gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item); + gtk_widget_show (item); +} + +static gboolean +chat_view_event_cb (GossipChatView *view, + GdkEventMotion *event, + GtkTextTag *tag) +{ + static GdkCursor *hand = NULL; + static GdkCursor *beam = NULL; + GtkTextWindowType type; + GtkTextIter iter; + GdkWindow *win; + gint x, y, buf_x, buf_y; + + type = gtk_text_view_get_window_type (GTK_TEXT_VIEW (view), + event->window); + + if (type != GTK_TEXT_WINDOW_TEXT) { + return FALSE; + } + + /* Get where the pointer really is. */ + win = gtk_text_view_get_window (GTK_TEXT_VIEW (view), type); + if (!win) { + return FALSE; + } + + gdk_window_get_pointer (win, &x, &y, NULL); + + /* Get the iter where the cursor is at */ + gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (view), type, + x, y, + &buf_x, &buf_y); + + gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (view), + &iter, + buf_x, buf_y); + + if (gtk_text_iter_has_tag (&iter, tag)) { + if (!hand) { + hand = gdk_cursor_new (GDK_HAND2); + beam = gdk_cursor_new (GDK_XTERM); + } + gdk_window_set_cursor (win, hand); + } else { + if (!beam) { + beam = gdk_cursor_new (GDK_XTERM); + } + gdk_window_set_cursor (win, beam); + } + + return FALSE; +} + +static gboolean +chat_view_url_event_cb (GtkTextTag *tag, + GObject *object, + GdkEvent *event, + GtkTextIter *iter, + GtkTextBuffer *buffer) +{ + GtkTextIter start, end; + gchar *str; + + /* If the link is being selected, don't do anything. */ + gtk_text_buffer_get_selection_bounds (buffer, &start, &end); + if (gtk_text_iter_get_offset (&start) != gtk_text_iter_get_offset (&end)) { + return FALSE; + } + + if (event->type == GDK_BUTTON_RELEASE && event->button.button == 1) { + start = end = *iter; + + if (gtk_text_iter_backward_to_tag_toggle (&start, tag) && + gtk_text_iter_forward_to_tag_toggle (&end, tag)) { + str = gtk_text_buffer_get_text (buffer, + &start, + &end, + FALSE); + + gossip_url_show (str); + g_free (str); + } + } + + return FALSE; +} + +static void +chat_view_open_address_cb (GtkMenuItem *menuitem, const gchar *url) +{ + gossip_url_show (url); +} + +static void +chat_view_copy_address_cb (GtkMenuItem *menuitem, const gchar *url) +{ + GtkClipboard *clipboard; + + clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD); + gtk_clipboard_set_text (clipboard, url, -1); + + clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY); + gtk_clipboard_set_text (clipboard, url, -1); +} + +static void +chat_view_clear_view_cb (GtkMenuItem *menuitem, GossipChatView *view) +{ + gossip_chat_view_clear (view); +} + +static void +chat_view_insert_text_with_emoticons (GtkTextBuffer *buf, + GtkTextIter *iter, + const gchar *str) +{ + const gchar *p; + gunichar c, prev_c; + gint i; + gint match; + gint submatch; + gboolean use_smileys = FALSE; + + gossip_conf_get_bool (gossip_conf_get (), + GOSSIP_PREFS_CHAT_SHOW_SMILEYS, + &use_smileys); + + if (!use_smileys) { + gtk_text_buffer_insert (buf, iter, str, -1); + return; + } + + while (*str) { + gint smileys_index[G_N_ELEMENTS (smileys)]; + GdkPixbuf *pixbuf; + gint len; + const gchar *start; + + memset (smileys_index, 0, sizeof (smileys_index)); + + match = -1; + submatch = -1; + p = str; + prev_c = 0; + + while (*p) { + c = g_utf8_get_char (p); + + if (match != -1 && g_unichar_isspace (c)) { + break; + } else { + match = -1; + } + + if (submatch != -1 || prev_c == 0 || g_unichar_isspace (prev_c)) { + submatch = -1; + + for (i = 0; i < G_N_ELEMENTS (smileys); i++) { + /* Only try to match if we already have + * a beginning match for the pattern, or + * if it's the first character in the + * pattern, if it's not in the middle of + * a word. + */ + if (((smileys_index[i] == 0 && (prev_c == 0 || g_unichar_isspace (prev_c))) || + smileys_index[i] > 0) && + smileys[i].pattern[smileys_index[i]] == c) { + submatch = i; + + smileys_index[i]++; + if (!smileys[i].pattern[smileys_index[i]]) { + match = i; + } + } else { + smileys_index[i] = 0; + } + } + } + + prev_c = c; + p = g_utf8_next_char (p); + } + + if (match == -1) { + gtk_text_buffer_insert (buf, iter, str, -1); + return; + } + + start = p - strlen (smileys[match].pattern); + + if (start > str) { + len = start - str; + gtk_text_buffer_insert (buf, iter, str, len); + } + + pixbuf = gossip_chat_view_get_smiley_image (smileys[match].smiley); + gtk_text_buffer_insert_pixbuf (buf, iter, pixbuf); + + gtk_text_buffer_insert (buf, iter, " ", 1); + + str = g_utf8_find_next_char (p, NULL); + } +} + +static gboolean +chat_view_is_scrolled_down (GossipChatView *view) +{ + GtkWidget *sw; + + sw = gtk_widget_get_parent (GTK_WIDGET (view)); + if (GTK_IS_SCROLLED_WINDOW (sw)) { + GtkAdjustment *vadj; + + vadj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (sw)); + + if (vadj->value + vadj->page_size / 2 < vadj->upper - vadj->page_size) { + return FALSE; + } + } + + return TRUE; +} + +static void +chat_view_maybe_trim_buffer (GossipChatView *view) +{ + GossipChatViewPriv *priv; + GtkTextIter top, bottom; + gint line; + gint remove; + GtkTextTagTable *table; + GtkTextTag *tag; + + priv = GET_PRIV (view); + + gtk_text_buffer_get_end_iter (priv->buffer, &bottom); + line = gtk_text_iter_get_line (&bottom); + if (line < MAX_LINES) { + return; + } + + remove = line - MAX_LINES; + gtk_text_buffer_get_start_iter (priv->buffer, &top); + + bottom = top; + if (!gtk_text_iter_forward_lines (&bottom, remove)) { + return; + } + + /* Track backwords to a place where we can safely cut, we don't do it in + * the middle of a tag. + */ + table = gtk_text_buffer_get_tag_table (priv->buffer); + tag = gtk_text_tag_table_lookup (table, "cut"); + if (!tag) { + return; + } + + if (!gtk_text_iter_forward_to_tag_toggle (&bottom, tag)) { + return; + } + + if (!gtk_text_iter_equal (&top, &bottom)) { + gtk_text_buffer_delete (priv->buffer, &top, &bottom); + } +} + +static void +chat_view_maybe_append_date_and_time (GossipChatView *view, + GossipMessage *msg) +{ + GossipChatViewPriv *priv; + const gchar *tag; + time_t timestamp; + GDate *date, *last_date; + GtkTextIter iter; + gboolean append_date, append_time; + GString *str; + + priv = GET_PRIV (view); + + if (priv->irc_style) { + tag = "irc-time"; + } else { + tag = "fancy-time"; + } + + if (priv->last_block_type == BLOCK_TYPE_TIME) { + return; + } + + str = g_string_new (NULL); + + timestamp = 0; + if (msg) { + timestamp = gossip_message_get_timestamp (msg); + } + + if (timestamp <= 0) { + timestamp = gossip_time_get_current (); + } + + date = g_date_new (); + g_date_set_time (date, timestamp); + + last_date = g_date_new (); + g_date_set_time (last_date, priv->last_timestamp); + + append_date = FALSE; + append_time = FALSE; + + if (g_date_compare (date, last_date) > 0) { + append_date = TRUE; + append_time = TRUE; + } + + if (priv->last_timestamp + TIMESTAMP_INTERVAL < timestamp) { + append_time = TRUE; + } + + if (append_time || append_date) { + chat_view_append_spacing (view); + + g_string_append (str, "- "); + } + + if (append_date) { + gchar buf[256]; + + g_date_strftime (buf, 256, _("%A %d %B %Y"), date); + g_string_append (str, buf); + + if (append_time) { + g_string_append (str, ", "); + } + } + + g_date_free (date); + g_date_free (last_date); + + if (append_time) { + gchar *tmp; + + tmp = gossip_time_to_string_local (timestamp, GOSSIP_TIME_FORMAT_DISPLAY_SHORT); + g_string_append (str, tmp); + g_free (tmp); + } + + if (append_time || append_date) { + g_string_append (str, " -\n"); + + gtk_text_buffer_get_end_iter (priv->buffer, &iter); + gtk_text_buffer_insert_with_tags_by_name (priv->buffer, + &iter, + str->str, -1, + tag, + NULL); + + priv->last_block_type = BLOCK_TYPE_TIME; + priv->last_timestamp = timestamp; + } + + g_string_free (str, TRUE); +} + +static void +chat_view_append_spacing (GossipChatView *view) +{ + GossipChatViewPriv *priv; + const gchar *tag; + GtkTextIter iter; + + priv = GET_PRIV (view); + + if (priv->irc_style) { + tag = "irc-spacing"; + } else { + tag = "fancy-spacing"; + } + + gtk_text_buffer_get_end_iter (priv->buffer, &iter); + gtk_text_buffer_insert_with_tags_by_name (priv->buffer, + &iter, + "\n", + -1, + "cut", + tag, + NULL); +} + +static void +chat_view_append_text (GossipChatView *view, + const gchar *body, + const gchar *tag) +{ + GossipChatViewPriv *priv; + GtkTextIter start_iter, end_iter; + GtkTextMark *mark; + GtkTextIter iter; + gint num_matches, i; + GArray *start, *end; + const gchar *link_tag; + + priv = GET_PRIV (view); + + if (priv->irc_style) { + link_tag = "irc-link"; + } else { + link_tag = "fancy-link"; + } + + gtk_text_buffer_get_end_iter (priv->buffer, &start_iter); + mark = gtk_text_buffer_create_mark (priv->buffer, NULL, &start_iter, TRUE); + + start = g_array_new (FALSE, FALSE, sizeof (gint)); + end = g_array_new (FALSE, FALSE, sizeof (gint)); + + num_matches = gossip_regex_match (GOSSIP_REGEX_ALL, body, start, end); + + if (num_matches == 0) { + gtk_text_buffer_get_end_iter (priv->buffer, &iter); + chat_view_insert_text_with_emoticons (priv->buffer, &iter, body); + } else { + gint last = 0; + gint s = 0, e = 0; + gchar *tmp; + + for (i = 0; i < num_matches; i++) { + s = g_array_index (start, gint, i); + e = g_array_index (end, gint, i); + + if (s > last) { + tmp = gossip_substring (body, last, s); + + gtk_text_buffer_get_end_iter (priv->buffer, &iter); + chat_view_insert_text_with_emoticons (priv->buffer, + &iter, + tmp); + g_free (tmp); + } + + tmp = gossip_substring (body, s, e); + + gtk_text_buffer_get_end_iter (priv->buffer, &iter); + gtk_text_buffer_insert_with_tags_by_name (priv->buffer, + &iter, + tmp, + -1, + link_tag, + "link", + NULL); + + g_free (tmp); + + last = e; + } + + if (e < strlen (body)) { + tmp = gossip_substring (body, e, strlen (body)); + + gtk_text_buffer_get_end_iter (priv->buffer, &iter); + chat_view_insert_text_with_emoticons (priv->buffer, + &iter, + tmp); + g_free (tmp); + } + } + + g_array_free (start, TRUE); + g_array_free (end, TRUE); + + gtk_text_buffer_get_end_iter (priv->buffer, &iter); + gtk_text_buffer_insert (priv->buffer, &iter, "\n", 1); + + /* Apply the style to the inserted text. */ + gtk_text_buffer_get_iter_at_mark (priv->buffer, &start_iter, mark); + gtk_text_buffer_get_end_iter (priv->buffer, &end_iter); + + gtk_text_buffer_apply_tag_by_name (priv->buffer, + tag, + &start_iter, + &end_iter); + + gtk_text_buffer_delete_mark (priv->buffer, mark); +} + +static void +chat_view_maybe_append_fancy_header (GossipChatView *view, + GossipMessage *msg) +{ + GossipChatViewPriv *priv; + GossipContact *sender; + GossipContact *my_contact; + const gchar *name; + gboolean header; + GtkTextIter iter; + gchar *tmp; + const gchar *tag; + const gchar *avatar_tag; + const gchar *line_top_tag; + const gchar *line_bottom_tag; + gboolean from_self; + GdkPixbuf *avatar; + + priv = GET_PRIV (view); + + sender = gossip_message_get_sender (msg); + my_contact = gossip_get_own_contact_from_contact (sender); + name = gossip_contact_get_name (sender); + from_self = gossip_contact_equal (sender, my_contact); + + gossip_debug (DEBUG_DOMAIN, "Maybe add fancy header"); + + if (from_self) { + tag = "fancy-header-self"; + line_top_tag = "fancy-line-top-self"; + line_bottom_tag = "fancy-line-bottom-self"; + } else { + tag = "fancy-header-other"; + line_top_tag = "fancy-line-top-other"; + line_bottom_tag = "fancy-line-bottom-other"; + } + + header = FALSE; + + /* Only insert a header if the previously inserted block is not the same + * as this one. This catches all the different cases: + */ + if (priv->last_block_type != BLOCK_TYPE_SELF && + priv->last_block_type != BLOCK_TYPE_OTHER) { + header = TRUE; + } + else if (from_self && priv->last_block_type == BLOCK_TYPE_OTHER) { + header = TRUE; + } + else if (!from_self && priv->last_block_type == BLOCK_TYPE_SELF) { + header = TRUE; + } + else if (!from_self && + (!priv->last_contact || + !gossip_contact_equal (sender, priv->last_contact))) { + header = TRUE; + } + + if (!header) { + return; + } + + chat_view_append_spacing (view); + + gtk_text_buffer_get_end_iter (priv->buffer, &iter); + gtk_text_buffer_insert_with_tags_by_name (priv->buffer, + &iter, + "\n", + -1, + line_top_tag, + NULL); + + avatar = gossip_pixbuf_avatar_from_contact_scaled (sender, 32, 32); + + if (avatar) { + GtkTextIter start; + + gtk_text_buffer_get_end_iter (priv->buffer, &iter); + gtk_text_buffer_insert_pixbuf (priv->buffer, &iter, avatar); + + gtk_text_buffer_get_end_iter (priv->buffer, &iter); + start = iter; + gtk_text_iter_backward_char (&start); + + if (from_self) { + gtk_text_buffer_apply_tag_by_name (priv->buffer, + "fancy-avatar-self", + &start, &iter); + avatar_tag = "fancy-header-self-avatar"; + } else { + gtk_text_buffer_apply_tag_by_name (priv->buffer, + "fancy-avatar-other", + &start, &iter); + avatar_tag = "fancy-header-other-avatar"; + } + + g_object_unref (avatar); + } else { + avatar_tag = NULL; + } + + tmp = g_strdup_printf ("%s\n", name); + + gtk_text_buffer_get_end_iter (priv->buffer, &iter); + gtk_text_buffer_insert_with_tags_by_name (priv->buffer, + &iter, + tmp, + -1, + tag, + avatar_tag, + NULL); + g_free (tmp); + + gtk_text_buffer_get_end_iter (priv->buffer, &iter); + gtk_text_buffer_insert_with_tags_by_name (priv->buffer, + &iter, + "\n", + -1, + line_bottom_tag, + NULL); +} + +static void +chat_view_append_irc_action (GossipChatView *view, + GossipMessage *msg) +{ + GossipChatViewPriv *priv; + GossipContact *my_contact; + GossipContact *sender; + const gchar *name; + GtkTextIter iter; + const gchar *body; + gchar *tmp; + const gchar *tag; + + priv = GET_PRIV (view); + + gossip_debug (DEBUG_DOMAIN, "Add IRC action"); + + sender = gossip_message_get_sender (msg); + my_contact = gossip_get_own_contact_from_contact (sender); + name = gossip_contact_get_name (sender); + + /* Skip the "/me ". */ + if (gossip_contact_equal (sender, my_contact)) { + tag = "irc-action-self"; + } else { + tag = "irc-action-other"; + } + + if (priv->last_block_type != BLOCK_TYPE_SELF && + priv->last_block_type != BLOCK_TYPE_OTHER) { + chat_view_append_spacing (view); + } + + gtk_text_buffer_get_end_iter (priv->buffer, &iter); + + tmp = g_strdup_printf (" * %s ", name); + gtk_text_buffer_insert_with_tags_by_name (priv->buffer, + &iter, + tmp, + -1, + "cut", + tag, + NULL); + g_free (tmp); + + body = gossip_message_get_body (msg); + chat_view_append_text (view, body, tag); +} + +static void +chat_view_append_fancy_action (GossipChatView *view, + GossipMessage *msg) +{ + GossipChatViewPriv *priv; + GossipContact *sender; + GossipContact *my_contact; + const gchar *name; + const gchar *body; + GtkTextIter iter; + gchar *tmp; + const gchar *tag; + const gchar *line_tag; + + priv = GET_PRIV (view); + + gossip_debug (DEBUG_DOMAIN, "Add fancy action"); + + sender = gossip_message_get_sender (msg); + my_contact = gossip_get_own_contact_from_contact (sender); + name = gossip_contact_get_name (sender); + + if (gossip_contact_equal (sender, my_contact)) { + tag = "fancy-action-self"; + line_tag = "fancy-line-self"; + } else { + tag = "fancy-action-other"; + line_tag = "fancy-line-other"; + } + + tmp = g_strdup_printf (" * %s ", name); + gtk_text_buffer_get_end_iter (priv->buffer, &iter); + gtk_text_buffer_insert_with_tags_by_name (priv->buffer, + &iter, + tmp, + -1, + tag, + NULL); + g_free (tmp); + + body = gossip_message_get_body (msg); + chat_view_append_text (view, body, tag); +} + +static void +chat_view_append_irc_message (GossipChatView *view, + GossipMessage *msg) +{ + GossipChatViewPriv *priv; + GossipContact *sender; + GossipContact *my_contact; + const gchar *name; + const gchar *body; + const gchar *nick_tag; + const gchar *body_tag; + GtkTextIter iter; + gchar *tmp; + + priv = GET_PRIV (view); + + gossip_debug (DEBUG_DOMAIN, "Add IRC message"); + + body = gossip_message_get_body (msg); + sender = gossip_message_get_sender (msg); + my_contact = gossip_get_own_contact_from_contact (sender); + name = gossip_contact_get_name (sender); + + if (gossip_contact_equal (sender, my_contact)) { + nick_tag = "irc-nick-self"; + body_tag = "irc-body-self"; + } else { + if (gossip_chat_should_highlight_nick (msg)) { + nick_tag = "irc-nick-highlight"; + } else { + nick_tag = "irc-nick-other"; + } + + body_tag = "irc-body-other"; + } + + if (priv->last_block_type != BLOCK_TYPE_SELF && + priv->last_block_type != BLOCK_TYPE_OTHER) { + chat_view_append_spacing (view); + } + + gtk_text_buffer_get_end_iter (priv->buffer, &iter); + + /* The nickname. */ + tmp = g_strdup_printf ("%s: ", name); + gtk_text_buffer_insert_with_tags_by_name (priv->buffer, + &iter, + tmp, + -1, + "cut", + nick_tag, + NULL); + g_free (tmp); + + /* The text body. */ + chat_view_append_text (view, body, body_tag); +} + +static void +chat_view_append_fancy_message (GossipChatView *view, + GossipMessage *msg) +{ + GossipChatViewPriv *priv; + GossipContact *sender; + GossipContact *my_contact; + const gchar *body; + const gchar *tag; + + priv = GET_PRIV (view); + + sender = gossip_message_get_sender (msg); + my_contact = gossip_get_own_contact_from_contact (sender); + + if (gossip_contact_equal (sender, my_contact)) { + tag = "fancy-body-self"; + } else { + tag = "fancy-body-other"; + + /* FIXME: Might want to support nick highlighting here... */ + } + + body = gossip_message_get_body (msg); + chat_view_append_text (view, body, tag); +} + +static void +chat_view_theme_changed_cb (GossipThemeManager *manager, + GossipChatView *view) +{ + GossipChatViewPriv *priv; + gboolean show_avatars = FALSE; + gboolean theme_rooms = FALSE; + + priv = GET_PRIV (view); + + priv->last_block_type = BLOCK_TYPE_NONE; + + gossip_conf_get_bool (gossip_conf_get (), + GOSSIP_PREFS_CHAT_THEME_CHAT_ROOM, + &theme_rooms); + if (!theme_rooms && priv->is_group_chat) { + gossip_theme_manager_apply (manager, view, NULL); + } else { + gossip_theme_manager_apply_saved (manager, view); + } + + /* Needed for now to update the "rise" property of the names to get it + * vertically centered. + */ + gossip_conf_get_bool (gossip_conf_get (), + GOSSIP_PREFS_UI_SHOW_AVATARS, + &show_avatars); + gossip_theme_manager_update_show_avatars (manager, view, show_avatars); +} + +GossipChatView * +gossip_chat_view_new (void) +{ + return g_object_new (GOSSIP_TYPE_CHAT_VIEW, NULL); +} + +/* The name is optional, if NULL, the sender for msg is used. */ +void +gossip_chat_view_append_message (GossipChatView *view, + GossipMessage *msg) +{ + GossipChatViewPriv *priv; + GossipContact *sender; + const gchar *body; + gboolean scroll_down; + + g_return_if_fail (GOSSIP_IS_CHAT_VIEW (view)); + g_return_if_fail (GOSSIP_IS_MESSAGE (msg)); + + priv = GET_PRIV (view); + + body = gossip_message_get_body (msg); + if (!body) { + return; + } + + scroll_down = chat_view_is_scrolled_down (view); + + chat_view_maybe_trim_buffer (view); + chat_view_maybe_append_date_and_time (view, msg); + + sender = gossip_message_get_sender (msg); + + if (!priv->irc_style) { + chat_view_maybe_append_fancy_header (view, msg); + } + + if (gossip_message_get_type (msg) == GOSSIP_MESSAGE_TYPE_ACTION) { + if (priv->irc_style) { + chat_view_append_irc_action (view, msg); + } else { + chat_view_append_fancy_action (view, msg); + } + } else { + if (priv->irc_style) { + chat_view_append_irc_message (view, msg); + } else { + chat_view_append_fancy_message (view, msg); + } + } + + priv->last_block_type = BLOCK_TYPE_SELF; + + /* Reset the last inserted contact, since it was from self. */ + if (priv->last_contact) { + g_object_unref (priv->last_contact); + priv->last_contact = NULL; + } + + if (scroll_down) { + gossip_chat_view_scroll_down (view); + } +} + +void +gossip_chat_view_append_event (GossipChatView *view, + const gchar *str) +{ + GossipChatViewPriv *priv; + gboolean bottom; + GtkTextIter iter; + gchar *msg; + const gchar *tag; + + g_return_if_fail (GOSSIP_IS_CHAT_VIEW (view)); + g_return_if_fail (!G_STR_EMPTY (str)); + + priv = GET_PRIV (view); + + bottom = chat_view_is_scrolled_down (view); + + chat_view_maybe_trim_buffer (view); + + if (priv->irc_style) { + tag = "irc-event"; + msg = g_strdup_printf (" - %s\n", str); + } else { + tag = "fancy-event"; + msg = g_strdup_printf (" - %s\n", str); + } + + if (priv->last_block_type != BLOCK_TYPE_EVENT) { + /* Comment out for now. */ + /*chat_view_append_spacing (view);*/ + } + + chat_view_maybe_append_date_and_time (view, NULL); + + gtk_text_buffer_get_end_iter (priv->buffer, &iter); + + gtk_text_buffer_insert_with_tags_by_name (priv->buffer, &iter, + msg, -1, + tag, + NULL); + g_free (msg); + + if (bottom) { + gossip_chat_view_scroll_down (view); + } + + priv->last_block_type = BLOCK_TYPE_EVENT; +} + +void +gossip_chat_view_append_button (GossipChatView *view, + const gchar *message, + GtkWidget *button1, + GtkWidget *button2) +{ + GossipChatViewPriv *priv; + GtkTextChildAnchor *anchor; + GtkTextIter iter; + gboolean bottom; + const gchar *tag; + + g_return_if_fail (GOSSIP_IS_CHAT_VIEW (view)); + g_return_if_fail (button1 != NULL); + + priv = GET_PRIV (view); + + if (priv->irc_style) { + tag = "irc-invite"; + } else { + tag = "fancy-invite"; + } + + bottom = chat_view_is_scrolled_down (view); + + chat_view_maybe_append_date_and_time (view, NULL); + + if (message) { + chat_view_append_text (view, message, tag); + } + + gtk_text_buffer_get_end_iter (priv->buffer, &iter); + + anchor = gtk_text_buffer_create_child_anchor (priv->buffer, &iter); + gtk_text_view_add_child_at_anchor (GTK_TEXT_VIEW (view), button1, anchor); + gtk_widget_show (button1); + + gtk_text_buffer_insert_with_tags_by_name (priv->buffer, + &iter, + " ", + 1, + tag, + NULL); + + if (button2) { + gtk_text_buffer_get_end_iter (priv->buffer, &iter); + + anchor = gtk_text_buffer_create_child_anchor (priv->buffer, &iter); + gtk_text_view_add_child_at_anchor (GTK_TEXT_VIEW (view), button2, anchor); + gtk_widget_show (button2); + + gtk_text_buffer_insert_with_tags_by_name (priv->buffer, + &iter, + " ", + 1, + tag, + NULL); + } + + gtk_text_buffer_get_end_iter (priv->buffer, &iter); + gtk_text_buffer_insert_with_tags_by_name (priv->buffer, + &iter, + "\n\n", + 2, + tag, + NULL); + + if (bottom) { + gossip_chat_view_scroll_down (view); + } + + priv->last_block_type = BLOCK_TYPE_INVITE; +} + +void +gossip_chat_view_scroll (GossipChatView *view, + gboolean allow_scrolling) +{ + GossipChatViewPriv *priv; + + g_return_if_fail (GOSSIP_IS_CHAT_VIEW (view)); + + priv = GET_PRIV (view); + + priv->allow_scrolling = allow_scrolling; + + gossip_debug (DEBUG_DOMAIN, "Scrolling %s", + allow_scrolling ? "enabled" : "disabled"); +} + +void +gossip_chat_view_scroll_down (GossipChatView *view) +{ + GossipChatViewPriv *priv; + GtkTextBuffer *buffer; + GtkTextIter iter; + GtkTextMark *mark; + + g_return_if_fail (GOSSIP_IS_CHAT_VIEW (view)); + + priv = GET_PRIV (view); + + if (!priv->allow_scrolling) { + return; + } + + gossip_debug (DEBUG_DOMAIN, "Scrolling down"); + + buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)); + + gtk_text_buffer_get_end_iter (buffer, &iter); + mark = gtk_text_buffer_create_mark (buffer, + NULL, + &iter, + FALSE); + + gtk_text_view_scroll_to_mark (GTK_TEXT_VIEW (view), + mark, + 0.0, + FALSE, + 0, + 0); + + gtk_text_buffer_delete_mark (buffer, mark); +} + +gboolean +gossip_chat_view_get_selection_bounds (GossipChatView *view, + GtkTextIter *start, + GtkTextIter *end) +{ + GtkTextBuffer *buffer; + + g_return_val_if_fail (GOSSIP_IS_CHAT_VIEW (view), FALSE); + + buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)); + + return gtk_text_buffer_get_selection_bounds (buffer, start, end); +} + +void +gossip_chat_view_clear (GossipChatView *view) +{ + GtkTextBuffer *buffer; + GossipChatViewPriv *priv; + + g_return_if_fail (GOSSIP_IS_CHAT_VIEW (view)); + + buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)); + gtk_text_buffer_set_text (buffer, "", -1); + + /* We set these back to the initial values so we get + * timestamps when clearing the window to know when + * conversations start. + */ + priv = GET_PRIV (view); + + priv->last_block_type = BLOCK_TYPE_NONE; + priv->last_timestamp = 0; +} + +gboolean +gossip_chat_view_find_previous (GossipChatView *view, + const gchar *search_criteria, + gboolean new_search) +{ + GossipChatViewPriv *priv; + GtkTextBuffer *buffer; + GtkTextIter iter_at_mark; + GtkTextIter iter_match_start; + GtkTextIter iter_match_end; + gboolean found; + gboolean from_start = FALSE; + + g_return_val_if_fail (GOSSIP_IS_CHAT_VIEW (view), FALSE); + g_return_val_if_fail (search_criteria != NULL, FALSE); + + priv = GET_PRIV (view); + + buffer = priv->buffer; + + if (G_STR_EMPTY (search_criteria)) { + if (priv->find_mark_previous) { + gtk_text_buffer_get_start_iter (buffer, &iter_at_mark); + + gtk_text_buffer_move_mark (buffer, + priv->find_mark_previous, + &iter_at_mark); + gtk_text_view_scroll_to_mark (GTK_TEXT_VIEW (view), + priv->find_mark_previous, + 0.0, + TRUE, + 0.0, + 0.0); + gtk_text_buffer_select_range (buffer, + &iter_at_mark, + &iter_at_mark); + } + + return FALSE; + } + + if (new_search) { + from_start = TRUE; + } + + if (priv->find_mark_previous) { + gtk_text_buffer_get_iter_at_mark (buffer, + &iter_at_mark, + priv->find_mark_previous); + } else { + gtk_text_buffer_get_end_iter (buffer, &iter_at_mark); + from_start = TRUE; + } + + priv->find_last_direction = FALSE; + + found = gossip_text_iter_backward_search (&iter_at_mark, + search_criteria, + &iter_match_start, + &iter_match_end, + NULL); + + if (!found) { + gboolean result = FALSE; + + if (from_start) { + return result; + } + + /* Here we wrap around. */ + if (!new_search && !priv->find_wrapped) { + priv->find_wrapped = TRUE; + result = gossip_chat_view_find_previous (view, + search_criteria, + FALSE); + priv->find_wrapped = FALSE; + } + + return result; + } + + /* Set new mark and show on screen */ + if (!priv->find_mark_previous) { + priv->find_mark_previous = gtk_text_buffer_create_mark (buffer, NULL, + &iter_match_start, + TRUE); + } else { + gtk_text_buffer_move_mark (buffer, + priv->find_mark_previous, + &iter_match_start); + } + + if (!priv->find_mark_next) { + priv->find_mark_next = gtk_text_buffer_create_mark (buffer, NULL, + &iter_match_end, + TRUE); + } else { + gtk_text_buffer_move_mark (buffer, + priv->find_mark_next, + &iter_match_end); + } + + gtk_text_view_scroll_to_mark (GTK_TEXT_VIEW (view), + priv->find_mark_previous, + 0.0, + TRUE, + 0.5, + 0.5); + + gtk_text_buffer_move_mark_by_name (buffer, "selection_bound", &iter_match_start); + gtk_text_buffer_move_mark_by_name (buffer, "insert", &iter_match_end); + + return TRUE; +} + +gboolean +gossip_chat_view_find_next (GossipChatView *view, + const gchar *search_criteria, + gboolean new_search) +{ + GossipChatViewPriv *priv; + GtkTextBuffer *buffer; + GtkTextIter iter_at_mark; + GtkTextIter iter_match_start; + GtkTextIter iter_match_end; + gboolean found; + gboolean from_start = FALSE; + + g_return_val_if_fail (GOSSIP_IS_CHAT_VIEW (view), FALSE); + g_return_val_if_fail (search_criteria != NULL, FALSE); + + priv = GET_PRIV (view); + + buffer = priv->buffer; + + if (G_STR_EMPTY (search_criteria)) { + if (priv->find_mark_next) { + gtk_text_buffer_get_start_iter (buffer, &iter_at_mark); + + gtk_text_buffer_move_mark (buffer, + priv->find_mark_next, + &iter_at_mark); + gtk_text_view_scroll_to_mark (GTK_TEXT_VIEW (view), + priv->find_mark_next, + 0.0, + TRUE, + 0.0, + 0.0); + gtk_text_buffer_select_range (buffer, + &iter_at_mark, + &iter_at_mark); + } + + return FALSE; + } + + if (new_search) { + from_start = TRUE; + } + + if (priv->find_mark_next) { + gtk_text_buffer_get_iter_at_mark (buffer, + &iter_at_mark, + priv->find_mark_next); + } else { + gtk_text_buffer_get_start_iter (buffer, &iter_at_mark); + from_start = TRUE; + } + + priv->find_last_direction = TRUE; + + found = gossip_text_iter_forward_search (&iter_at_mark, + search_criteria, + &iter_match_start, + &iter_match_end, + NULL); + + if (!found) { + gboolean result = FALSE; + + if (from_start) { + return result; + } + + /* Here we wrap around. */ + if (!new_search && !priv->find_wrapped) { + priv->find_wrapped = TRUE; + result = gossip_chat_view_find_next (view, + search_criteria, + FALSE); + priv->find_wrapped = FALSE; + } + + return result; + } + + /* Set new mark and show on screen */ + if (!priv->find_mark_next) { + priv->find_mark_next = gtk_text_buffer_create_mark (buffer, NULL, + &iter_match_end, + TRUE); + } else { + gtk_text_buffer_move_mark (buffer, + priv->find_mark_next, + &iter_match_end); + } + + if (!priv->find_mark_previous) { + priv->find_mark_previous = gtk_text_buffer_create_mark (buffer, NULL, + &iter_match_start, + TRUE); + } else { + gtk_text_buffer_move_mark (buffer, + priv->find_mark_previous, + &iter_match_start); + } + + gtk_text_view_scroll_to_mark (GTK_TEXT_VIEW (view), + priv->find_mark_next, + 0.0, + TRUE, + 0.5, + 0.5); + + gtk_text_buffer_move_mark_by_name (buffer, "selection_bound", &iter_match_start); + gtk_text_buffer_move_mark_by_name (buffer, "insert", &iter_match_end); + + return TRUE; +} + + +void +gossip_chat_view_find_abilities (GossipChatView *view, + const gchar *search_criteria, + gboolean *can_do_previous, + gboolean *can_do_next) +{ + GossipChatViewPriv *priv; + GtkTextBuffer *buffer; + GtkTextIter iter_at_mark; + GtkTextIter iter_match_start; + GtkTextIter iter_match_end; + + g_return_if_fail (GOSSIP_IS_CHAT_VIEW (view)); + g_return_if_fail (search_criteria != NULL); + g_return_if_fail (can_do_previous != NULL && can_do_next != NULL); + + priv = GET_PRIV (view); + + buffer = priv->buffer; + + if (can_do_previous) { + if (priv->find_mark_previous) { + gtk_text_buffer_get_iter_at_mark (buffer, + &iter_at_mark, + priv->find_mark_previous); + } else { + gtk_text_buffer_get_start_iter (buffer, &iter_at_mark); + } + + *can_do_previous = gossip_text_iter_backward_search (&iter_at_mark, + search_criteria, + &iter_match_start, + &iter_match_end, + NULL); + } + + if (can_do_next) { + if (priv->find_mark_next) { + gtk_text_buffer_get_iter_at_mark (buffer, + &iter_at_mark, + priv->find_mark_next); + } else { + gtk_text_buffer_get_start_iter (buffer, &iter_at_mark); + } + + *can_do_next = gossip_text_iter_forward_search (&iter_at_mark, + search_criteria, + &iter_match_start, + &iter_match_end, + NULL); + } +} + +void +gossip_chat_view_highlight (GossipChatView *view, + const gchar *text) +{ + GtkTextBuffer *buffer; + GtkTextIter iter; + GtkTextIter iter_start; + GtkTextIter iter_end; + GtkTextIter iter_match_start; + GtkTextIter iter_match_end; + gboolean found; + + g_return_if_fail (GOSSIP_IS_CHAT_VIEW (view)); + + buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)); + + gtk_text_buffer_get_start_iter (buffer, &iter); + + gtk_text_buffer_get_bounds (buffer, &iter_start, &iter_end); + gtk_text_buffer_remove_tag_by_name (buffer, "highlight", + &iter_start, + &iter_end); + + if (G_STR_EMPTY (text)) { + return; + } + + while (1) { + found = gossip_text_iter_forward_search (&iter, + text, + &iter_match_start, + &iter_match_end, + NULL); + + if (!found) { + break; + } + + gtk_text_buffer_apply_tag_by_name (buffer, "highlight", + &iter_match_start, + &iter_match_end); + + iter = iter_match_end; + gtk_text_iter_forward_char (&iter); + } +} + +void +gossip_chat_view_copy_clipboard (GossipChatView *view) +{ + GtkTextBuffer *buffer; + GtkClipboard *clipboard; + + g_return_if_fail (GOSSIP_IS_CHAT_VIEW (view)); + + buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)); + clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD); + + gtk_text_buffer_copy_clipboard (buffer, clipboard); +} + +gboolean +gossip_chat_view_get_irc_style (GossipChatView *view) +{ + GossipChatViewPriv *priv; + + g_return_val_if_fail (GOSSIP_IS_CHAT_VIEW (view), FALSE); + + priv = GET_PRIV (view); + + return priv->irc_style; +} + +void +gossip_chat_view_set_irc_style (GossipChatView *view, + gboolean irc_style) +{ + GossipChatViewPriv *priv; + + g_return_if_fail (GOSSIP_IS_CHAT_VIEW (view)); + + priv = GET_PRIV (view); + + priv->irc_style = irc_style; +} + +void +gossip_chat_view_set_margin (GossipChatView *view, + gint margin) +{ + GossipChatViewPriv *priv; + + g_return_if_fail (GOSSIP_IS_CHAT_VIEW (view)); + + priv = GET_PRIV (view); + + g_object_set (view, + "left-margin", margin, + "right-margin", margin, + NULL); +} + +GdkPixbuf * +gossip_chat_view_get_smiley_image (GossipSmiley smiley) +{ + static GdkPixbuf *pixbufs[GOSSIP_SMILEY_COUNT]; + static gboolean inited = FALSE; + + if (!inited) { + gint i; + + for (i = 0; i < GOSSIP_SMILEY_COUNT; i++) { + pixbufs[i] = gossip_pixbuf_from_smiley (i, GTK_ICON_SIZE_MENU); + } + + inited = TRUE; + } + + return pixbufs[smiley]; +} + +const gchar * +gossip_chat_view_get_smiley_text (GossipSmiley smiley) +{ + gint i; + + for (i = 0; i < G_N_ELEMENTS (smileys); i++) { + if (smileys[i].smiley != smiley) { + continue; + } + + return smileys[i].pattern; + } + + return NULL; +} + +GtkWidget * +gossip_chat_view_get_smiley_menu (GCallback callback, + gpointer user_data, + GtkTooltips *tooltips) +{ + GtkWidget *menu; + gint x; + gint y; + gint i; + + g_return_val_if_fail (callback != NULL, NULL); + + menu = gtk_menu_new (); + + for (i = 0, x = 0, y = 0; i < GOSSIP_SMILEY_COUNT; i++) { + GtkWidget *item; + GtkWidget *image; + GdkPixbuf *pixbuf; + const gchar *smiley_text; + + pixbuf = gossip_chat_view_get_smiley_image (i); + if (!pixbuf) { + continue; + } + + image = gtk_image_new_from_pixbuf (pixbuf); + + item = gtk_image_menu_item_new_with_label (""); + gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image); + + gtk_menu_attach (GTK_MENU (menu), item, + x, x + 1, y, y + 1); + + smiley_text = gossip_chat_view_get_smiley_text (i); + + gtk_tooltips_set_tip (tooltips, + item, + smiley_text, + NULL); + + g_object_set_data (G_OBJECT (item), "smiley_text", (gpointer) smiley_text); + g_signal_connect (item, "activate", callback, user_data); + + if (x > 3) { + y++; + x = 0; + } else { + x++; + } + } + + gtk_widget_show_all (menu); + + return menu; +} + +/* FIXME: Do we really need this? Better to do it internally only at setup time, + * we will never change it on the fly. + */ +void +gossip_chat_view_set_is_group_chat (GossipChatView *view, + gboolean is_group_chat) +{ + GossipChatViewPriv *priv; + gboolean theme_rooms = FALSE; + + g_return_if_fail (GOSSIP_IS_CHAT_VIEW (view)); + + priv = GET_PRIV (view); + + priv->is_group_chat = is_group_chat; + + gossip_conf_get_bool (gossip_conf_get (), + GOSSIP_PREFS_CHAT_THEME_CHAT_ROOM, + &theme_rooms); + + if (!theme_rooms && is_group_chat) { + gossip_theme_manager_apply (gossip_theme_manager_get (), + view, + NULL); + } else { + gossip_theme_manager_apply_saved (gossip_theme_manager_get (), + view); + } +} diff --git a/libempathy-gtk/gossip-chat-view.h b/libempathy-gtk/gossip-chat-view.h new file mode 100644 index 00000000..2a7b1147 --- /dev/null +++ b/libempathy-gtk/gossip-chat-view.h @@ -0,0 +1,134 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2002-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Mikael Hallendal <micke@imendio.com> + * Richard Hult <richard@imendio.com> + * Martyn Russell <martyn@imendio.com> + */ + +#ifndef __GOSSIP_CHAT_VIEW_H__ +#define __GOSSIP_CHAT_VIEW_H__ + +#include <gtk/gtktextview.h> +#include <gtk/gtktooltips.h> + +#include <libempathy/gossip-contact.h> +#include <libempathy/gossip-message.h> + +G_BEGIN_DECLS + +#define GOSSIP_TYPE_CHAT_VIEW (gossip_chat_view_get_type ()) +#define GOSSIP_CHAT_VIEW(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GOSSIP_TYPE_CHAT_VIEW, GossipChatView)) +#define GOSSIP_CHAT_VIEW_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GOSSIP_TYPE_CHAT_VIEW, GossipChatViewClass)) +#define GOSSIP_IS_CHAT_VIEW(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GOSSIP_TYPE_CHAT_VIEW)) +#define GOSSIP_IS_CHAT_VIEW_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GOSSIP_TYPE_CHAT_VIEW)) +#define GOSSIP_CHAT_VIEW_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GOSSIP_TYPE_CHAT_VIEW, GossipChatViewClass)) + +typedef struct _GossipChatView GossipChatView; +typedef struct _GossipChatViewClass GossipChatViewClass; +typedef struct _GossipChatViewPriv GossipChatViewPriv; + +struct _GossipChatView { + GtkTextView parent; +}; + +struct _GossipChatViewClass { + GtkTextViewClass parent_class; +}; + +typedef enum { + GOSSIP_SMILEY_NORMAL, /* :) */ + GOSSIP_SMILEY_WINK, /* ;) */ + GOSSIP_SMILEY_BIGEYE, /* =) */ + GOSSIP_SMILEY_NOSE, /* :-) */ + GOSSIP_SMILEY_CRY, /* :'( */ + GOSSIP_SMILEY_SAD, /* :( */ + GOSSIP_SMILEY_SCEPTICAL, /* :/ */ + GOSSIP_SMILEY_BIGSMILE, /* :D */ + GOSSIP_SMILEY_INDIFFERENT, /* :| */ + GOSSIP_SMILEY_TOUNGE, /* :p */ + GOSSIP_SMILEY_SHOCKED, /* :o */ + GOSSIP_SMILEY_COOL, /* 8) */ + GOSSIP_SMILEY_SORRY, /* *| */ + GOSSIP_SMILEY_KISS, /* :* */ + GOSSIP_SMILEY_SHUTUP, /* :# */ + GOSSIP_SMILEY_YAWN, /* |O */ + GOSSIP_SMILEY_CONFUSED, /* :$ */ + GOSSIP_SMILEY_ANGEL, /* <) */ + GOSSIP_SMILEY_OOOH, /* :x */ + GOSSIP_SMILEY_LOOKAWAY, /* *) */ + GOSSIP_SMILEY_BLUSH, /* *S */ + GOSSIP_SMILEY_COOLBIGSMILE, /* 8D */ + GOSSIP_SMILEY_ANGRY, /* :@ */ + GOSSIP_SMILEY_BOSS, /* @) */ + GOSSIP_SMILEY_MONKEY, /* #) */ + GOSSIP_SMILEY_SILLY, /* O) */ + GOSSIP_SMILEY_SICK, /* +o( */ + + GOSSIP_SMILEY_COUNT +} GossipSmiley; + +GType gossip_chat_view_get_type (void) G_GNUC_CONST; +GossipChatView *gossip_chat_view_new (void); +void gossip_chat_view_append_message (GossipChatView *view, + GossipMessage *msg); +void gossip_chat_view_append_event (GossipChatView *view, + const gchar *str); +void gossip_chat_view_append_button (GossipChatView *view, + const gchar *message, + GtkWidget *button1, + GtkWidget *button2); +void gossip_chat_view_set_margin (GossipChatView *view, + gint margin); +void gossip_chat_view_scroll (GossipChatView *view, + gboolean allow_scrolling); +void gossip_chat_view_scroll_down (GossipChatView *view); +gboolean gossip_chat_view_get_selection_bounds (GossipChatView *view, + GtkTextIter *start, + GtkTextIter *end); +void gossip_chat_view_clear (GossipChatView *view); +gboolean gossip_chat_view_find_previous (GossipChatView *view, + const gchar *search_criteria, + gboolean new_search); +gboolean gossip_chat_view_find_next (GossipChatView *view, + const gchar *search_criteria, + gboolean new_search); +void gossip_chat_view_find_abilities (GossipChatView *view, + const gchar *search_criteria, + gboolean *can_do_previous, + gboolean *can_do_next); +void gossip_chat_view_highlight (GossipChatView *view, + const gchar *text); +void gossip_chat_view_copy_clipboard (GossipChatView *view); +gboolean gossip_chat_view_get_irc_style (GossipChatView *view); +void gossip_chat_view_set_irc_style (GossipChatView *view, + gboolean irc_style); +void gossip_chat_view_set_margin (GossipChatView *view, + gint margin); +GdkPixbuf * gossip_chat_view_get_smiley_image (GossipSmiley smiley); +const gchar * gossip_chat_view_get_smiley_text (GossipSmiley smiley); +GtkWidget * gossip_chat_view_get_smiley_menu (GCallback callback, + gpointer user_data, + GtkTooltips *tooltips); +void gossip_chat_view_set_is_group_chat (GossipChatView *view, + gboolean is_group_chat); + +G_END_DECLS + +#endif /* __GOSSIP_CHAT_VIEW_H__ */ diff --git a/libempathy-gtk/gossip-chat-window.c b/libempathy-gtk/gossip-chat-window.c new file mode 100644 index 00000000..da305e60 --- /dev/null +++ b/libempathy-gtk/gossip-chat-window.c @@ -0,0 +1,1894 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2003-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Mikael Hallendal <micke@imendio.com> + * Richard Hult <richard@imendio.com> + * Martyn Russell <martyn@imendio.com> + * Geert-Jan Van den Bogaerde <geertjan@gnome.org> + */ + +#include "config.h" + +#include <string.h> + +#include <gtk/gtk.h> +#include <gdk/gdkkeysyms.h> +#include <glade/glade.h> +#include <glib/gi18n.h> + +#include <libempathy/gossip-contact.h> +#include <libempathy/gossip-debug.h> +#include <libempathy/gossip-message.h> +#include <libempathy/gossip-conf.h> + +#include "gossip-chat-window.h" +//#include "gossip-add-contact-dialog.h" +//#include "gossip-chat-invite.h" +//#include "gossip-contact-info-dialog.h" +//#include "gossip-log-window.h" +//#include "gossip-new-chatroom-dialog.h" +#include "gossip-preferences.h" +#include "gossip-private-chat.h" +//#include "gossip-sound.h" +#include "gossip-stock.h" +#include "gossip-ui-utils.h" + +#define GET_PRIV(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GOSSIP_TYPE_CHAT_WINDOW, GossipChatWindowPriv)) + +#define DEBUG_DOMAIN "ChatWindow" + +#define URGENCY_TIMEOUT 60*1000 + +struct _GossipChatWindowPriv { + GList *chats; + GList *chats_new_msg; + GList *chats_composing; + + GossipChat *current_chat; + + gboolean page_added; + gboolean dnd_same_window; + + guint urgency_timeout_id; + + GtkWidget *dialog; + GtkWidget *notebook; + + GtkTooltips *tooltips; + + /* Menu items. */ + GtkWidget *menu_conv_clear; + GtkWidget *menu_conv_insert_smiley; + GtkWidget *menu_conv_log; + GtkWidget *menu_conv_separator; + GtkWidget *menu_conv_add_contact; + GtkWidget *menu_conv_info; + GtkWidget *menu_conv_close; + + GtkWidget *menu_room; + GtkWidget *menu_room_set_topic; + GtkWidget *menu_room_join_new; + GtkWidget *menu_room_invite; + GtkWidget *menu_room_add; + GtkWidget *menu_room_show_contacts; + + GtkWidget *menu_edit_cut; + GtkWidget *menu_edit_copy; + GtkWidget *menu_edit_paste; + + GtkWidget *menu_tabs_next; + GtkWidget *menu_tabs_prev; + GtkWidget *menu_tabs_left; + GtkWidget *menu_tabs_right; + GtkWidget *menu_tabs_detach; + + guint save_geometry_id; +}; + +static void gossip_chat_window_class_init (GossipChatWindowClass *klass); +static void gossip_chat_window_init (GossipChatWindow *window); +static void gossip_chat_window_finalize (GObject *object); +static GdkPixbuf *chat_window_get_status_pixbuf (GossipChatWindow *window, + GossipChat *chat); +static void chat_window_accel_cb (GtkAccelGroup *accelgroup, + GObject *object, + guint key, + GdkModifierType mod, + GossipChatWindow *window); +static void chat_window_close_clicked_cb (GtkWidget *button, + GossipChat *chat); +static GtkWidget *chat_window_create_label (GossipChatWindow *window, + GossipChat *chat); +static void chat_window_update_status (GossipChatWindow *window, + GossipChat *chat); +static void chat_window_update_title (GossipChatWindow *window, + GossipChat *chat); +static void chat_window_update_menu (GossipChatWindow *window); +static gboolean chat_window_save_geometry_timeout_cb (GossipChatWindow *window); +static gboolean chat_window_configure_event_cb (GtkWidget *widget, + GdkEventConfigure *event, + GossipChatWindow *window); +static void chat_window_conv_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window); +static void chat_window_clear_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window); +static void chat_window_info_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window); +static void chat_window_add_contact_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window); +static void chat_window_log_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window); +static void chat_window_show_contacts_toggled_cb (GtkWidget *menuitem, + GossipChatWindow *window); +static void chat_window_edit_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window); +static void chat_window_insert_smiley_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window); +static void chat_window_close_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window); +static void chat_window_room_set_topic_activate_cb(GtkWidget *menuitem, + GossipChatWindow *window); +static void chat_window_room_join_new_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window); +static void chat_window_room_invite_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window); +static void chat_window_room_add_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window); +static void chat_window_cut_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window); +static void chat_window_copy_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window); +static void chat_window_paste_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window); +static void chat_window_tabs_left_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window); +static void chat_window_tabs_right_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window); +static void chat_window_detach_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window); +static gboolean chat_window_delete_event_cb (GtkWidget *dialog, + GdkEvent *event, + GossipChatWindow *window); +static void chat_window_status_changed_cb (GossipChat *chat, + GossipChatWindow *window); +static void chat_window_update_tooltip (GossipChatWindow *window, + GossipChat *chat); +static void chat_window_name_changed_cb (GossipChat *chat, + const gchar *name, + GossipChatWindow *window); +static void chat_window_composing_cb (GossipChat *chat, + gboolean is_composing, + GossipChatWindow *window); +static void chat_window_new_message_cb (GossipChat *chat, + GossipMessage *message, + GossipChatWindow *window); +static GtkNotebook* chat_window_detach_hook (GtkNotebook *source, + GtkWidget *page, + gint x, + gint y, + gpointer user_data); +static void chat_window_page_switched_cb (GtkNotebook *notebook, + GtkNotebookPage *page, + gint page_num, + GossipChatWindow *window); +static void chat_window_page_reordered_cb (GtkNotebook *notebook, + GtkWidget *widget, + guint page_num, + GossipChatWindow *window); +static void chat_window_page_added_cb (GtkNotebook *notebook, + GtkWidget *child, + guint page_num, + GossipChatWindow *window); +static void chat_window_page_removed_cb (GtkNotebook *notebook, + GtkWidget *child, + guint page_num, + GossipChatWindow *window); +static gboolean chat_window_focus_in_event_cb (GtkWidget *widget, + GdkEvent *event, + GossipChatWindow *window); +static void chat_window_drag_data_received (GtkWidget *widget, + GdkDragContext *context, + int x, + int y, + GtkSelectionData *selection, + guint info, + guint time, + GossipChatWindow *window); +static void chat_window_set_urgency_hint (GossipChatWindow *window, + gboolean urgent); + + +static GList *chat_windows = NULL; + +static const guint tab_accel_keys[] = { + GDK_1, GDK_2, GDK_3, GDK_4, GDK_5, + GDK_6, GDK_7, GDK_8, GDK_9, GDK_0 +}; + +typedef enum { + DND_DRAG_TYPE_CONTACT_ID, + DND_DRAG_TYPE_TAB +} DndDragType; + +static const GtkTargetEntry drag_types_dest[] = { + { "text/contact-id", GTK_TARGET_SAME_APP, DND_DRAG_TYPE_CONTACT_ID }, + { "GTK_NOTEBOOK_TAB", GTK_TARGET_SAME_APP, DND_DRAG_TYPE_TAB }, +}; + +G_DEFINE_TYPE (GossipChatWindow, gossip_chat_window, G_TYPE_OBJECT); + +static void +gossip_chat_window_class_init (GossipChatWindowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gossip_chat_window_finalize; + + g_type_class_add_private (object_class, sizeof (GossipChatWindowPriv)); + + /* Set up a style for the close button with no focus padding. */ + gtk_rc_parse_string ( + "style \"gossip-close-button-style\"\n" + "{\n" + " GtkWidget::focus-padding = 0\n" + " xthickness = 0\n" + " ythickness = 0\n" + "}\n" + "widget \"*.gossip-close-button\" style \"gossip-close-button-style\""); + + gtk_notebook_set_window_creation_hook (chat_window_detach_hook, NULL, NULL); +} + +static void +gossip_chat_window_init (GossipChatWindow *window) +{ + GossipChatWindowPriv *priv; + GladeXML *glade; + GtkAccelGroup *accel_group; + GtkWidget *image; + GClosure *closure; + GtkWidget *menu_conv; + GtkWidget *menu; + gint i; + GtkWidget *chat_vbox; + + priv = GET_PRIV (window); + + priv->tooltips = g_object_ref (gtk_tooltips_new ()); + gtk_object_sink (GTK_OBJECT (priv->tooltips)); + + glade = gossip_glade_get_file ("empathy-chat.glade", + "chat_window", + NULL, + "chat_window", &priv->dialog, + "chat_vbox", &chat_vbox, + "menu_conv", &menu_conv, + "menu_conv_clear", &priv->menu_conv_clear, + "menu_conv_insert_smiley", &priv->menu_conv_insert_smiley, + "menu_conv_log", &priv->menu_conv_log, + "menu_conv_separator", &priv->menu_conv_separator, + "menu_conv_add_contact", &priv->menu_conv_add_contact, + "menu_conv_info", &priv->menu_conv_info, + "menu_conv_close", &priv->menu_conv_close, + "menu_room", &priv->menu_room, + "menu_room_set_topic", &priv->menu_room_set_topic, + "menu_room_join_new", &priv->menu_room_join_new, + "menu_room_invite", &priv->menu_room_invite, + "menu_room_add", &priv->menu_room_add, + "menu_room_show_contacts", &priv->menu_room_show_contacts, + "menu_edit_cut", &priv->menu_edit_cut, + "menu_edit_copy", &priv->menu_edit_copy, + "menu_edit_paste", &priv->menu_edit_paste, + "menu_tabs_next", &priv->menu_tabs_next, + "menu_tabs_prev", &priv->menu_tabs_prev, + "menu_tabs_left", &priv->menu_tabs_left, + "menu_tabs_right", &priv->menu_tabs_right, + "menu_tabs_detach", &priv->menu_tabs_detach, + NULL); + + gossip_glade_connect (glade, + window, + "chat_window", "configure-event", chat_window_configure_event_cb, + "menu_conv", "activate", chat_window_conv_activate_cb, + "menu_conv_clear", "activate", chat_window_clear_activate_cb, + "menu_conv_log", "activate", chat_window_log_activate_cb, + "menu_conv_add_contact", "activate", chat_window_add_contact_activate_cb, + "menu_conv_info", "activate", chat_window_info_activate_cb, + "menu_conv_close", "activate", chat_window_close_activate_cb, + "menu_room_set_topic", "activate", chat_window_room_set_topic_activate_cb, + "menu_room_join_new", "activate", chat_window_room_join_new_activate_cb, + "menu_room_invite", "activate", chat_window_room_invite_activate_cb, + "menu_room_add", "activate", chat_window_room_add_activate_cb, + "menu_edit", "activate", chat_window_edit_activate_cb, + "menu_edit_cut", "activate", chat_window_cut_activate_cb, + "menu_edit_copy", "activate", chat_window_copy_activate_cb, + "menu_edit_paste", "activate", chat_window_paste_activate_cb, + "menu_tabs_left", "activate", chat_window_tabs_left_activate_cb, + "menu_tabs_right", "activate", chat_window_tabs_right_activate_cb, + "menu_tabs_detach", "activate", chat_window_detach_activate_cb, + NULL); + + g_object_unref (glade); + + priv->notebook = gtk_notebook_new (); + gtk_notebook_set_group_id (GTK_NOTEBOOK (priv->notebook), 1); + gtk_box_pack_start (GTK_BOX (chat_vbox), priv->notebook, TRUE, TRUE, 0); + gtk_widget_show (priv->notebook); + + /* Set up accels */ + accel_group = gtk_accel_group_new (); + gtk_window_add_accel_group (GTK_WINDOW (priv->dialog), accel_group); + + for (i = 0; i < G_N_ELEMENTS (tab_accel_keys); i++) { + closure = g_cclosure_new (G_CALLBACK (chat_window_accel_cb), + window, + NULL); + gtk_accel_group_connect (accel_group, + tab_accel_keys[i], + GDK_MOD1_MASK, + 0, + closure); + } + + g_object_unref (accel_group); + + /* Set the contact information menu item image to the Gossip + * stock image + */ + image = gtk_image_menu_item_get_image (GTK_IMAGE_MENU_ITEM (priv->menu_conv_info)); + gtk_image_set_from_stock (GTK_IMAGE (image), + GOSSIP_STOCK_CONTACT_INFORMATION, + GTK_ICON_SIZE_MENU); + + /* Set up smiley menu */ + menu = gossip_chat_view_get_smiley_menu ( + G_CALLBACK (chat_window_insert_smiley_activate_cb), + window, + priv->tooltips); + gtk_menu_item_set_submenu (GTK_MENU_ITEM (priv->menu_conv_insert_smiley), menu); + + /* Set up signals we can't do with glade since we may need to + * block/unblock them at some later stage. + */ + + g_signal_connect (priv->dialog, + "delete_event", + G_CALLBACK (chat_window_delete_event_cb), + window); + + g_signal_connect (priv->menu_room_show_contacts, + "toggled", + G_CALLBACK (chat_window_show_contacts_toggled_cb), + window); + + g_signal_connect_swapped (priv->menu_tabs_prev, + "activate", + G_CALLBACK (gtk_notebook_prev_page), + priv->notebook); + g_signal_connect_swapped (priv->menu_tabs_next, + "activate", + G_CALLBACK (gtk_notebook_next_page), + priv->notebook); + + g_signal_connect (priv->dialog, + "focus_in_event", + G_CALLBACK (chat_window_focus_in_event_cb), + window); + g_signal_connect_after (priv->notebook, + "switch_page", + G_CALLBACK (chat_window_page_switched_cb), + window); + g_signal_connect (priv->notebook, + "page_reordered", + G_CALLBACK (chat_window_page_reordered_cb), + window); + g_signal_connect (priv->notebook, + "page_added", + G_CALLBACK (chat_window_page_added_cb), + window); + g_signal_connect (priv->notebook, + "page_removed", + G_CALLBACK (chat_window_page_removed_cb), + window); + + /* Set up drag and drop */ + gtk_drag_dest_set (GTK_WIDGET (priv->notebook), + GTK_DEST_DEFAULT_ALL, + drag_types_dest, + G_N_ELEMENTS (drag_types_dest), + GDK_ACTION_MOVE); + + g_signal_connect (priv->notebook, + "drag-data-received", + G_CALLBACK (chat_window_drag_data_received), + window); + + chat_windows = g_list_prepend (chat_windows, window); + + /* Set up private details */ + priv->chats = NULL; + priv->chats_new_msg = NULL; + priv->chats_composing = NULL; + priv->current_chat = NULL; +} + +/* Returns the window to open a new tab in if there is only one window + * visble, otherwise, returns NULL indicating that a new window should + * be added. + */ +GossipChatWindow * +gossip_chat_window_get_default (void) +{ + GList *l; + gboolean separate_windows = TRUE; + + gossip_conf_get_bool (gossip_conf_get (), + GOSSIP_PREFS_UI_SEPARATE_CHAT_WINDOWS, + &separate_windows); + + if (separate_windows) { + /* Always create a new window */ + return NULL; + } + + for (l = chat_windows; l; l = l->next) { + GossipChatWindow *chat_window; + GtkWidget *dialog; + GdkWindow *window; + gboolean visible; + + chat_window = l->data; + + dialog = gossip_chat_window_get_dialog (chat_window); + window = dialog->window; + + g_object_get (dialog, + "visible", &visible, + NULL); + + visible = visible && !(gdk_window_get_state (window) & GDK_WINDOW_STATE_ICONIFIED); + + if (visible) { + /* Found a visible window on this desktop */ + return chat_window; + } + } + + return NULL; +} + +static void +gossip_chat_window_finalize (GObject *object) +{ + GossipChatWindow *window; + GossipChatWindowPriv *priv; + + window = GOSSIP_CHAT_WINDOW (object); + priv = GET_PRIV (window); + + gossip_debug (DEBUG_DOMAIN, "Finalized: %p", object); + + if (priv->save_geometry_id != 0) { + g_source_remove (priv->save_geometry_id); + } + + if (priv->urgency_timeout_id != 0) { + g_source_remove (priv->urgency_timeout_id); + } + + chat_windows = g_list_remove (chat_windows, window); + gtk_widget_destroy (priv->dialog); + + g_object_unref (priv->tooltips); + + G_OBJECT_CLASS (gossip_chat_window_parent_class)->finalize (object); +} + +static GdkPixbuf * +chat_window_get_status_pixbuf (GossipChatWindow *window, + GossipChat *chat) +{ + GossipChatWindowPriv *priv; + GdkPixbuf *pixbuf; + + priv = GET_PRIV (window); + + if (g_list_find (priv->chats_new_msg, chat)) { + pixbuf = gossip_stock_render (GOSSIP_STOCK_MESSAGE, + GTK_ICON_SIZE_MENU); + } + else if (g_list_find (priv->chats_composing, chat)) { + pixbuf = gossip_stock_render (GOSSIP_STOCK_TYPING, + GTK_ICON_SIZE_MENU); + } + else { + pixbuf = gossip_chat_get_status_pixbuf (chat); + } + + return pixbuf; +} + +static void +chat_window_accel_cb (GtkAccelGroup *accelgroup, + GObject *object, + guint key, + GdkModifierType mod, + GossipChatWindow *window) +{ + GossipChatWindowPriv *priv; + gint num = -1; + gint i; + + priv = GET_PRIV (window); + + for (i = 0; i < G_N_ELEMENTS (tab_accel_keys); i++) { + if (tab_accel_keys[i] == key) { + num = i; + break; + } + } + + if (num != -1) { + gtk_notebook_set_current_page (GTK_NOTEBOOK (priv->notebook), num); + } +} + +static void +chat_window_close_clicked_cb (GtkWidget *button, + GossipChat *chat) +{ + GossipChatWindow *window; + + window = gossip_chat_get_window (chat); + gossip_chat_window_remove_chat (window, chat); +} + +static void +chat_window_close_button_style_set_cb (GtkWidget *button, + GtkStyle *previous_style, + gpointer user_data) +{ + gint h, w; + + gtk_icon_size_lookup_for_settings (gtk_widget_get_settings (button), + GTK_ICON_SIZE_MENU, &w, &h); + + gtk_widget_set_size_request (button, w, h); +} + +static GtkWidget * +chat_window_create_label (GossipChatWindow *window, + GossipChat *chat) +{ + GossipChatWindowPriv *priv; + GtkWidget *hbox; + GtkWidget *name_label; + GtkWidget *status_image; + GtkWidget *close_button; + GtkWidget *close_image; + GtkWidget *event_box; + GtkWidget *event_box_hbox; + PangoAttrList *attr_list; + PangoAttribute *attr; + + priv = GET_PRIV (window); + + /* The spacing between the button and the label. */ + hbox = gtk_hbox_new (FALSE, 0); + + event_box = gtk_event_box_new (); + gtk_event_box_set_visible_window (GTK_EVENT_BOX (event_box), FALSE); + + name_label = gtk_label_new (gossip_chat_get_name (chat)); + gtk_label_set_ellipsize (GTK_LABEL (name_label), PANGO_ELLIPSIZE_END); + + attr_list = pango_attr_list_new (); + attr = pango_attr_scale_new (1/1.2); + attr->start_index = 0; + attr->end_index = -1; + pango_attr_list_insert (attr_list, attr); + gtk_label_set_attributes (GTK_LABEL (name_label), attr_list); + pango_attr_list_unref (attr_list); + + gtk_misc_set_padding (GTK_MISC (name_label), 2, 0); + gtk_misc_set_alignment (GTK_MISC (name_label), 0.0, 0.5); + g_object_set_data (G_OBJECT (chat), "chat-window-tab-label", name_label); + + status_image = gtk_image_new (); + + /* Spacing between the icon and label. */ + event_box_hbox = gtk_hbox_new (FALSE, 0); + + gtk_box_pack_start (GTK_BOX (event_box_hbox), status_image, FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (event_box_hbox), name_label, TRUE, TRUE, 0); + + g_object_set_data (G_OBJECT (chat), "chat-window-tab-image", status_image); + g_object_set_data (G_OBJECT (chat), "chat-window-tab-tooltip-widget", event_box); + + close_button = gtk_button_new (); + gtk_button_set_relief (GTK_BUTTON (close_button), GTK_RELIEF_NONE); + + /* We don't want focus/keynav for the button to avoid clutter, and + * Ctrl-W works anyway. + */ + GTK_WIDGET_UNSET_FLAGS (close_button, GTK_CAN_FOCUS); + GTK_WIDGET_UNSET_FLAGS (close_button, GTK_CAN_DEFAULT); + + /* Set the name to make the special rc style match. */ + gtk_widget_set_name (close_button, "gossip-close-button"); + + close_image = gtk_image_new_from_stock (GTK_STOCK_CLOSE, GTK_ICON_SIZE_MENU); + + gtk_container_add (GTK_CONTAINER (close_button), close_image); + + gtk_container_add (GTK_CONTAINER (event_box), event_box_hbox); + gtk_box_pack_start (GTK_BOX (hbox), event_box, TRUE, TRUE, 0); + gtk_box_pack_end (GTK_BOX (hbox), close_button, FALSE, FALSE, 0); + + /* React to theme changes and also used to setup the initial size + * correctly. + */ + g_signal_connect (close_button, + "style-set", + G_CALLBACK (chat_window_close_button_style_set_cb), + chat); + + g_signal_connect (close_button, + "clicked", + G_CALLBACK (chat_window_close_clicked_cb), + chat); + + /* Set up tooltip */ + chat_window_update_tooltip (window, chat); + + gtk_widget_show_all (hbox); + + return hbox; +} + +static void +chat_window_update_status (GossipChatWindow *window, + GossipChat *chat) +{ + GtkImage *image; + GdkPixbuf *pixbuf; + + pixbuf = chat_window_get_status_pixbuf (window, chat); + image = g_object_get_data (G_OBJECT (chat), "chat-window-tab-image"); + gtk_image_set_from_pixbuf (image, pixbuf); + + g_object_unref (pixbuf); + + chat_window_update_title (window, chat); + chat_window_update_tooltip (window, chat); +} + +static void +chat_window_update_title (GossipChatWindow *window, + GossipChat *chat) +{ + GossipChatWindowPriv *priv; + GdkPixbuf *pixbuf = NULL; + const gchar *str; + gchar *title; + gint n_chats; + + priv = GET_PRIV (window); + + n_chats = g_list_length (priv->chats); + if (n_chats == 1) { + if (priv->chats_new_msg) { + title = g_strdup_printf ( + "%s - %s", + gossip_chat_get_name (priv->current_chat), + _("New Message")); + } + else if (gossip_chat_is_group_chat (priv->current_chat)) { + title = g_strdup_printf ( + "%s - %s", + gossip_chat_get_name (priv->current_chat), + _("Chat Room")); + } else { + title = g_strdup_printf ( + "%s - %s", + gossip_chat_get_name (priv->current_chat), + _("Conversation")); + } + } else { + if (priv->chats_new_msg) { + GString *names; + GList *l; + gint n_messages = 0; + + names = g_string_new (NULL); + + for (l = priv->chats_new_msg; l; l = l->next) { + n_messages++; + g_string_append (names, + gossip_chat_get_name (l->data)); + if (l->next) { + g_string_append (names, ", "); + } + } + + str = ngettext ("New Message", "New Messages", n_messages); + title = g_strdup_printf ("%s - %s", names->str, str); + g_string_free (names, TRUE); + } else { + str = ngettext ("Conversation", "Conversations (%d)", n_chats); + title = g_strdup_printf (str, n_chats); + } + } + + gtk_window_set_title (GTK_WINDOW (priv->dialog), title); + g_free (title); + + if (priv->chats_new_msg) { + pixbuf = gossip_stock_render (GOSSIP_STOCK_MESSAGE, + GTK_ICON_SIZE_MENU); + } else { + pixbuf = NULL; + } + + gtk_window_set_icon (GTK_WINDOW (priv->dialog), pixbuf); + + if (pixbuf) { + g_object_unref (pixbuf); + } +} + +static void +chat_window_update_menu (GossipChatWindow *window) +{ + GossipChatWindowPriv *priv; + gboolean first_page; + gboolean last_page; + gboolean is_connected; + gint num_pages; + gint page_num; + + priv = GET_PRIV (window); + + /* Notebook pages */ + page_num = gtk_notebook_get_current_page (GTK_NOTEBOOK (priv->notebook)); + num_pages = gtk_notebook_get_n_pages (GTK_NOTEBOOK (priv->notebook)); + first_page = (page_num == 0); + last_page = (page_num == (num_pages - 1)); + + gtk_widget_set_sensitive (priv->menu_tabs_next, !last_page); + gtk_widget_set_sensitive (priv->menu_tabs_prev, !first_page); + gtk_widget_set_sensitive (priv->menu_tabs_detach, num_pages > 1); + gtk_widget_set_sensitive (priv->menu_tabs_left, !first_page); + gtk_widget_set_sensitive (priv->menu_tabs_right, !last_page); + + is_connected = gossip_chat_is_connected (priv->current_chat); + + if (gossip_chat_is_group_chat (priv->current_chat)) { +#if 0 +FIXME: + GossipGroupChat *group_chat; + GossipChatroomManager *manager; + GossipChatroom *chatroom; + GossipChatroomId id; + gboolean saved; + + group_chat = GOSSIP_GROUP_CHAT (priv->current_chat); + chatroom = gossip_group_chat_get_chatroom (group_chat); + + /* Show / Hide widgets */ + gtk_widget_show (priv->menu_room); + + gtk_widget_hide (priv->menu_conv_add_contact); + gtk_widget_hide (priv->menu_conv_info); + gtk_widget_hide (priv->menu_conv_separator); + + /* Can we add this room to our favourites and are we + * connected to the room? + */ + manager = gossip_app_get_chatroom_manager (); + id = gossip_chatroom_get_id (chatroom); + saved = gossip_chatroom_manager_find (manager, id) != NULL; + + gtk_widget_set_sensitive (priv->menu_room_add, !saved); + gtk_widget_set_sensitive (priv->menu_conv_insert_smiley, is_connected); + gtk_widget_set_sensitive (priv->menu_room_join_new, is_connected); + gtk_widget_set_sensitive (priv->menu_room_invite, is_connected); + + /* We need to block the signal here because all we are + * really trying to do is check or uncheck the menu + * item. If we don't do this we get funny behaviour + * with 2 or more group chat windows where showing + * contacts doesn't do anything. + */ + show_contacts = gossip_chat_get_show_contacts (priv->current_chat); + + g_signal_handlers_block_by_func (priv->menu_room_show_contacts, + chat_window_show_contacts_toggled_cb, + window); + + g_object_set (priv->menu_room_show_contacts, + "active", show_contacts, + NULL); + + g_signal_handlers_unblock_by_func (priv->menu_room_show_contacts, + chat_window_show_contacts_toggled_cb, + window); +#endif + } else { + GossipSubscription subscription; + GossipContact *contact; + + /* Show / Hide widgets */ + gtk_widget_hide (priv->menu_room); + + contact = gossip_chat_get_contact (priv->current_chat); + subscription = gossip_contact_get_subscription (contact); + if (!(subscription & GOSSIP_SUBSCRIPTION_FROM)) { + gtk_widget_show (priv->menu_conv_add_contact); + } else { + gtk_widget_hide (priv->menu_conv_add_contact); + } + + gtk_widget_show (priv->menu_conv_separator); + gtk_widget_show (priv->menu_conv_info); + + /* Are we connected? */ + gtk_widget_set_sensitive (priv->menu_conv_insert_smiley, is_connected); + gtk_widget_set_sensitive (priv->menu_conv_add_contact, is_connected); + gtk_widget_set_sensitive (priv->menu_conv_info, is_connected); + } +} + +static void +chat_window_insert_smiley_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window) +{ + GossipChatWindowPriv *priv; + GossipChat *chat; + GtkTextBuffer *buffer; + GtkTextIter iter; + const gchar *smiley; + + priv = GET_PRIV (window); + + chat = priv->current_chat; + + smiley = g_object_get_data (G_OBJECT (menuitem), "smiley_text"); + + buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view)); + gtk_text_buffer_get_end_iter (buffer, &iter); + gtk_text_buffer_insert (buffer, &iter, + smiley, -1); +} + +static void +chat_window_clear_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window) +{ + GossipChatWindowPriv *priv; + + priv = GET_PRIV (window); + + gossip_chat_clear (priv->current_chat); +} + +static void +chat_window_add_contact_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window) +{ + GossipChatWindowPriv *priv; + GossipContact *contact; + + priv = GET_PRIV (window); + + contact = gossip_chat_get_contact (priv->current_chat); + + // FIXME: gossip_add_contact_dialog_show (NULL, contact); +} + +static void +chat_window_log_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window) +{ +/* FIXME: + GossipChatWindowPriv *priv; + + priv = GET_PRIV (window); + + if (gossip_chat_is_group_chat (priv->current_chat)) { + GossipGroupChat *group_chat; + GossipChatroom *chatroom; + + group_chat = GOSSIP_GROUP_CHAT (priv->current_chat); + chatroom = gossip_group_chat_get_chatroom (group_chat); + gossip_log_window_show (NULL, chatroom); + } else { + GossipContact *contact; + + contact = gossip_chat_get_contact (priv->current_chat); + gossip_log_window_show (contact, NULL); + } +*/ +} + +static void +chat_window_info_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window) +{ + GossipChatWindowPriv *priv; + GossipContact *contact; + + priv = GET_PRIV (window); + + contact = gossip_chat_get_contact (priv->current_chat); + +/*FIXME: gossip_contact_info_dialog_show (contact, + GTK_WINDOW (priv->dialog));*/ +} + +static gboolean +chat_window_save_geometry_timeout_cb (GossipChatWindow *window) +{ + GossipChatWindowPriv *priv; + gint x, y, w, h; + + priv = GET_PRIV (window); + + gtk_window_get_size (GTK_WINDOW (priv->dialog), &w, &h); + gtk_window_get_position (GTK_WINDOW (priv->dialog), &x, &y); + + gossip_chat_save_geometry (priv->current_chat, x, y, w, h); + + priv->save_geometry_id = 0; + + return FALSE; +} + +static gboolean +chat_window_configure_event_cb (GtkWidget *widget, + GdkEventConfigure *event, + GossipChatWindow *window) +{ + GossipChatWindowPriv *priv; + + priv = GET_PRIV (window); + + /* Only save geometry information if there is ONE chat visible. */ + if (g_list_length (priv->chats) > 1) { + return FALSE; + } + + if (priv->save_geometry_id != 0) { + g_source_remove (priv->save_geometry_id); + } + + priv->save_geometry_id = + g_timeout_add (500, + (GSourceFunc) chat_window_save_geometry_timeout_cb, + window); + + return FALSE; +} + +static void +chat_window_conv_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window) +{ + GossipChatWindowPriv *priv; + gboolean log_exists = FALSE; + + priv = GET_PRIV (window); +/* FIXME: + if (gossip_chat_is_group_chat (priv->current_chat)) { + GossipGroupChat *group_chat; + GossipChatroom *chatroom; + + group_chat = GOSSIP_GROUP_CHAT (priv->current_chat); + chatroom = gossip_group_chat_get_chatroom (group_chat); + if (chatroom) { + log_exists = gossip_log_exists_for_chatroom (chatroom); + } + } else { + GossipContact *contact; + + contact = gossip_chat_get_contact (priv->current_chat); + if (contact) { + log_exists = gossip_log_exists_for_contact (contact); + } + } +*/ + gtk_widget_set_sensitive (priv->menu_conv_log, log_exists); +} + +static void +chat_window_show_contacts_toggled_cb (GtkWidget *menuitem, + GossipChatWindow *window) +{ + GossipChatWindowPriv *priv; + gboolean show; + + priv = GET_PRIV (window); + + g_return_if_fail (priv->current_chat != NULL); + + show = gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (priv->menu_room_show_contacts)); + gossip_chat_set_show_contacts (priv->current_chat, show); +} + +static void +chat_window_close_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window) +{ + GossipChatWindowPriv *priv; + + priv = GET_PRIV (window); + + g_return_if_fail (priv->current_chat != NULL); + + gossip_chat_window_remove_chat (window, priv->current_chat); +} + +static void +chat_window_room_set_topic_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window) +{ +/*FIXME + GossipChatWindowPriv *priv; + + priv = GET_PRIV (window); + + if (gossip_chat_is_group_chat (priv->current_chat)) { + GossipGroupChat *group_chat; + + group_chat = GOSSIP_GROUP_CHAT (priv->current_chat); + gossip_group_chat_set_topic (group_chat); + }*/ +} + +static void +chat_window_room_join_new_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window) +{ + GossipChatWindowPriv *priv; + + priv = GET_PRIV (window); + + // FIXME: gossip_new_chatroom_dialog_show (GTK_WINDOW (priv->dialog)); +} + +static void +chat_window_room_invite_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window) +{ +/* FIXME: + GossipChatWindowPriv *priv; + GossipContact *own_contact; + GossipChatroomId id = 0; + + priv = GET_PRIV (window); + own_contact = gossip_chat_get_own_contact (priv->current_chat); + + if (gossip_chat_is_group_chat (priv->current_chat)) { + GossipGroupChat *group_chat; + + group_chat = GOSSIP_GROUP_CHAT (priv->current_chat); + id = gossip_group_chat_get_chatroom_id (group_chat); + } + + gossip_chat_invite_dialog_show (own_contact, id); +*/ +} + +static void +chat_window_room_add_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window) +{ +/* FIXME: + GossipChatWindowPriv *priv; + GossipGroupChat *group_chat; + GossipChatroomManager *manager; + GossipChatroom *chatroom; + + priv = GET_PRIV (window); + + g_return_if_fail (priv->current_chat != NULL); + + if (!gossip_chat_is_group_chat (priv->current_chat)) { + return; + } + + group_chat = GOSSIP_GROUP_CHAT (priv->current_chat); + chatroom = gossip_group_chat_get_chatroom (group_chat); + gossip_chatroom_set_favourite (chatroom, TRUE); + + manager = gossip_app_get_chatroom_manager (); + gossip_chatroom_manager_add (manager, chatroom); + gossip_chatroom_manager_store (manager); + + chat_window_update_menu (window); +*/ +} + +static void +chat_window_edit_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window) +{ + GossipChatWindowPriv *priv; + GtkClipboard *clipboard; + GtkTextBuffer *buffer; + gboolean text_available; + + priv = GET_PRIV (window); + + g_return_if_fail (priv->current_chat != NULL); + + if (!gossip_chat_is_connected (priv->current_chat)) { + gtk_widget_set_sensitive (priv->menu_edit_copy, FALSE); + gtk_widget_set_sensitive (priv->menu_edit_cut, FALSE); + gtk_widget_set_sensitive (priv->menu_edit_paste, FALSE); + return; + } + + buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (priv->current_chat->input_text_view)); + if (gtk_text_buffer_get_selection_bounds (buffer, NULL, NULL)) { + gtk_widget_set_sensitive (priv->menu_edit_copy, TRUE); + gtk_widget_set_sensitive (priv->menu_edit_cut, TRUE); + } else { + gboolean selection; + + selection = gossip_chat_view_get_selection_bounds (priv->current_chat->view, + NULL, NULL); + + gtk_widget_set_sensitive (priv->menu_edit_cut, FALSE); + gtk_widget_set_sensitive (priv->menu_edit_copy, selection); + } + + clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD); + text_available = gtk_clipboard_wait_is_text_available (clipboard); + gtk_widget_set_sensitive (priv->menu_edit_paste, text_available); +} + +static void +chat_window_cut_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window) +{ + GossipChatWindowPriv *priv; + + g_return_if_fail (GOSSIP_IS_CHAT_WINDOW (window)); + + priv = GET_PRIV (window); + + gossip_chat_cut (priv->current_chat); +} + +static void +chat_window_copy_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window) +{ + GossipChatWindowPriv *priv; + + g_return_if_fail (GOSSIP_IS_CHAT_WINDOW (window)); + + priv = GET_PRIV (window); + + gossip_chat_copy (priv->current_chat); +} + +static void +chat_window_paste_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window) +{ + GossipChatWindowPriv *priv; + + g_return_if_fail (GOSSIP_IS_CHAT_WINDOW (window)); + + priv = GET_PRIV (window); + + gossip_chat_paste (priv->current_chat); +} + +static void +chat_window_tabs_left_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window) +{ + GossipChatWindowPriv *priv; + GossipChat *chat; + gint index; + + priv = GET_PRIV (window); + + chat = priv->current_chat; + index = gtk_notebook_get_current_page (GTK_NOTEBOOK (priv->notebook)); + if (index <= 0) { + return; + } + + gtk_notebook_reorder_child (GTK_NOTEBOOK (priv->notebook), + gossip_chat_get_widget (chat), + index - 1); + + chat_window_update_menu (window); + chat_window_update_status (window, chat); +} + +static void +chat_window_tabs_right_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window) +{ + GossipChatWindowPriv *priv; + GossipChat *chat; + gint index; + + priv = GET_PRIV (window); + + chat = priv->current_chat; + index = gtk_notebook_get_current_page (GTK_NOTEBOOK (priv->notebook)); + + gtk_notebook_reorder_child (GTK_NOTEBOOK (priv->notebook), + gossip_chat_get_widget (chat), + index + 1); + + chat_window_update_menu (window); + chat_window_update_status (window, chat); +} + +static void +chat_window_detach_activate_cb (GtkWidget *menuitem, + GossipChatWindow *window) +{ + GossipChatWindowPriv *priv; + GossipChatWindow *new_window; + GossipChat *chat; + + priv = GET_PRIV (window); + + chat = priv->current_chat; + new_window = gossip_chat_window_new (); + + gossip_chat_window_move_chat (window, new_window, chat); + + priv = GET_PRIV (new_window); + gtk_widget_show (priv->dialog); +} + +static gboolean +chat_window_delete_event_cb (GtkWidget *dialog, + GdkEvent *event, + GossipChatWindow *window) +{ + GossipChatWindowPriv *priv; + GList *list; + GList *l; + + priv = GET_PRIV (window); + + gossip_debug (DEBUG_DOMAIN, "Delete event received"); + + list = g_list_copy (priv->chats); + + for (l = list; l; l = l->next) { + gossip_chat_window_remove_chat (window, l->data); + } + + g_list_free (list); + + return TRUE; +} + +static void +chat_window_status_changed_cb (GossipChat *chat, + GossipChatWindow *window) +{ + chat_window_update_menu (window); + chat_window_update_status (window, chat); +} + +static void +chat_window_update_tooltip (GossipChatWindow *window, + GossipChat *chat) +{ + GossipChatWindowPriv *priv; + GtkWidget *widget; + gchar *current_tooltip; + gchar *str; + + priv = GET_PRIV (window); + + current_tooltip = gossip_chat_get_tooltip (chat); + + if (g_list_find (priv->chats_composing, chat)) { + str = g_strconcat (current_tooltip, "\n", _("Typing a message."), NULL); + g_free (current_tooltip); + } else { + str = current_tooltip; + } + + widget = g_object_get_data (G_OBJECT (chat), "chat-window-tab-tooltip-widget"); + gtk_tooltips_set_tip (priv->tooltips, + widget, + str, + NULL); + + g_free (str); +} + +static void +chat_window_name_changed_cb (GossipChat *chat, + const gchar *name, + GossipChatWindow *window) +{ + GtkLabel *label; + + label = g_object_get_data (G_OBJECT (chat), "chat-window-tab-label"); + + gtk_label_set_text (label, name); +} + +static void +chat_window_composing_cb (GossipChat *chat, + gboolean is_composing, + GossipChatWindow *window) +{ + GossipChatWindowPriv *priv; + + priv = GET_PRIV (window); + + if (is_composing && !g_list_find (priv->chats_composing, chat)) { + priv->chats_composing = g_list_prepend (priv->chats_composing, chat); + } else { + priv->chats_composing = g_list_remove (priv->chats_composing, chat); + } + + chat_window_update_status (window, chat); +} + +static void +chat_window_new_message_cb (GossipChat *chat, + GossipMessage *message, + GossipChatWindow *window) +{ + GossipChatWindowPriv *priv; + gboolean has_focus; + gboolean needs_urgency; + + priv = GET_PRIV (window); + + has_focus = gossip_chat_window_has_focus (window); + + if (has_focus && priv->current_chat == chat) { + gossip_debug (DEBUG_DOMAIN, "New message, we have focus"); + return; + } + + gossip_debug (DEBUG_DOMAIN, "New message, no focus"); + + needs_urgency = FALSE; + if (gossip_chat_is_group_chat (chat)) { + if (gossip_chat_should_highlight_nick (message)) { + gossip_debug (DEBUG_DOMAIN, "Highlight this nick"); + needs_urgency = TRUE; + } + } else { + needs_urgency = TRUE; + } + + if (needs_urgency && !has_focus) { + chat_window_set_urgency_hint (window, TRUE); + } + + if (!g_list_find (priv->chats_new_msg, chat)) { + priv->chats_new_msg = g_list_prepend (priv->chats_new_msg, chat); + chat_window_update_status (window, chat); + } +} + +static GtkNotebook * +chat_window_detach_hook (GtkNotebook *source, + GtkWidget *page, + gint x, + gint y, + gpointer user_data) +{ + GossipChatWindowPriv *priv; + GossipChatWindow *window, *new_window; + GossipChat *chat; + + chat = g_object_get_data (G_OBJECT (page), "chat"); + window = gossip_chat_get_window (chat); + + new_window = gossip_chat_window_new (); + priv = GET_PRIV (new_window); + + gossip_debug (DEBUG_DOMAIN, "Detach hook called"); + + gossip_chat_window_move_chat (window, new_window, chat); + + gtk_window_move (GTK_WINDOW (priv->dialog), x, y); + gtk_widget_show (priv->dialog); + + return NULL; +} + +static void +chat_window_page_switched_cb (GtkNotebook *notebook, + GtkNotebookPage *page, + gint page_num, + GossipChatWindow *window) +{ + GossipChatWindowPriv *priv; + GossipChat *chat; + GtkWidget *child; + + gossip_debug (DEBUG_DOMAIN, "Page switched"); + + priv = GET_PRIV (window); + + child = gtk_notebook_get_nth_page (notebook, page_num); + chat = g_object_get_data (G_OBJECT (child), "chat"); + + if (priv->page_added) { + priv->page_added = FALSE; + gossip_chat_scroll_down (chat); + } + else if (priv->current_chat == chat) { + return; + } + + priv->current_chat = chat; + priv->chats_new_msg = g_list_remove (priv->chats_new_msg, chat); + + chat_window_update_menu (window); + chat_window_update_status (window, chat); +} + +static void +chat_window_page_reordered_cb (GtkNotebook *notebook, + GtkWidget *widget, + guint page_num, + GossipChatWindow *window) +{ + gossip_debug (DEBUG_DOMAIN, "Page reordered"); + + chat_window_update_menu (window); +} + +static void +chat_window_page_added_cb (GtkNotebook *notebook, + GtkWidget *child, + guint page_num, + GossipChatWindow *window) +{ + GossipChatWindowPriv *priv; + GossipChat *chat; + + priv = GET_PRIV (window); + + /* If we just received DND to the same window, we don't want + * to do anything here like removing the tab and then readding + * it, so we return here and in "page-added". + */ + if (priv->dnd_same_window) { + gossip_debug (DEBUG_DOMAIN, "Page added (back to the same window)"); + priv->dnd_same_window = FALSE; + return; + } + + gossip_debug (DEBUG_DOMAIN, "Page added"); + + /* Get chat object */ + chat = g_object_get_data (G_OBJECT (child), "chat"); + + /* Set the chat window */ + gossip_chat_set_window (chat, window); + + /* Connect chat signals for this window */ + g_signal_connect (chat, "status_changed", + G_CALLBACK (chat_window_status_changed_cb), + window); + g_signal_connect (chat, "name_changed", + G_CALLBACK (chat_window_name_changed_cb), + window); + g_signal_connect (chat, "composing", + G_CALLBACK (chat_window_composing_cb), + window); + g_signal_connect (chat, "new_message", + G_CALLBACK (chat_window_new_message_cb), + window); + + /* Set flag so we know to perform some special operations on + * switch page due to the new page being added. + */ + priv->page_added = TRUE; + + /* Get list of chats up to date */ + priv->chats = g_list_append (priv->chats, chat); +} + +static void +chat_window_page_removed_cb (GtkNotebook *notebook, + GtkWidget *child, + guint page_num, + GossipChatWindow *window) +{ + GossipChatWindowPriv *priv; + GossipChat *chat; + + priv = GET_PRIV (window); + + /* If we just received DND to the same window, we don't want + * to do anything here like removing the tab and then readding + * it, so we return here and in "page-added". + */ + if (priv->dnd_same_window) { + gossip_debug (DEBUG_DOMAIN, "Page removed (and will be readded to same window)"); + return; + } + + gossip_debug (DEBUG_DOMAIN, "Page removed"); + + /* Get chat object */ + chat = g_object_get_data (G_OBJECT (child), "chat"); + + /* Unset the window associated with a chat */ + gossip_chat_set_window (chat, NULL); + + /* Disconnect all signal handlers for this chat and this window */ + g_signal_handlers_disconnect_by_func (chat, + G_CALLBACK (chat_window_status_changed_cb), + window); + g_signal_handlers_disconnect_by_func (chat, + G_CALLBACK (chat_window_name_changed_cb), + window); + g_signal_handlers_disconnect_by_func (chat, + G_CALLBACK (chat_window_composing_cb), + window); + g_signal_handlers_disconnect_by_func (chat, + G_CALLBACK (chat_window_new_message_cb), + window); + + /* Keep list of chats up to date */ + priv->chats = g_list_remove (priv->chats, chat); + priv->chats_new_msg = g_list_remove (priv->chats_new_msg, chat); + priv->chats_composing = g_list_remove (priv->chats_composing, chat); + + if (priv->chats == NULL) { + g_object_unref (window); + } else { + chat_window_update_menu (window); + chat_window_update_title (window, NULL); + } +} + +static gboolean +chat_window_focus_in_event_cb (GtkWidget *widget, + GdkEvent *event, + GossipChatWindow *window) +{ + GossipChatWindowPriv *priv; + + gossip_debug (DEBUG_DOMAIN, "Focus in event, updating title"); + + priv = GET_PRIV (window); + + priv->chats_new_msg = g_list_remove (priv->chats_new_msg, priv->current_chat); + + chat_window_set_urgency_hint (window, FALSE); + + /* Update the title, since we now mark all unread messages as read. */ + chat_window_update_status (window, priv->current_chat); + + return FALSE; +} + +static void +chat_window_drag_data_received (GtkWidget *widget, + GdkDragContext *context, + int x, + int y, + GtkSelectionData *selection, + guint info, + guint time, + GossipChatWindow *window) +{ + if (info == DND_DRAG_TYPE_CONTACT_ID) { +#if 0 +FIXME: + GossipChatManager *manager; + GossipContact *contact; + GossipChat *chat; + GossipChatWindow *old_window; + const gchar *id = NULL; + + if (selection) { + id = (const gchar*) selection->data; + } + + gossip_debug (DEBUG_DOMAIN, "DND contact from roster with id:'%s'", id); + + contact = gossip_session_find_contact (gossip_app_get_session (), id); + if (!contact) { + gossip_debug (DEBUG_DOMAIN, "DND contact from roster not found"); + return; + } + + manager = gossip_app_get_chat_manager (); + chat = GOSSIP_CHAT (gossip_chat_manager_get_chat (manager, contact)); + old_window = gossip_chat_get_window (chat); + + if (old_window) { + if (old_window == window) { + gtk_drag_finish (context, TRUE, FALSE, time); + return; + } + + gossip_chat_window_move_chat (old_window, window, chat); + } else { + gossip_chat_window_add_chat (window, chat); + } + + /* Added to take care of any outstanding chat events */ + gossip_chat_manager_show_chat (manager, contact); + + /* We should return TRUE to remove the data when doing + * GDK_ACTION_MOVE, but we don't here otherwise it has + * weird consequences, and we handle that internally + * anyway with add_chat() and remove_chat(). + */ + gtk_drag_finish (context, TRUE, FALSE, time); +#endif + } + else if (info == DND_DRAG_TYPE_TAB) { + GossipChat *chat = NULL; + GossipChatWindow *old_window; + GtkWidget **child = NULL; + + gossip_debug (DEBUG_DOMAIN, "DND tab"); + + if (selection) { + child = (void*) selection->data; + } + + if (child) { + chat = g_object_get_data (G_OBJECT (*child), "chat"); + } + + old_window = gossip_chat_get_window (chat); + if (old_window) { + GossipChatWindowPriv *priv; + + priv = GET_PRIV (window); + + if (old_window == window) { + gossip_debug (DEBUG_DOMAIN, "DND tab (within same window)"); + priv->dnd_same_window = TRUE; + gtk_drag_finish (context, TRUE, FALSE, time); + return; + } + + priv->dnd_same_window = FALSE; + } + + /* We should return TRUE to remove the data when doing + * GDK_ACTION_MOVE, but we don't here otherwise it has + * weird consequences, and we handle that internally + * anyway with add_chat() and remove_chat(). + */ + gtk_drag_finish (context, TRUE, FALSE, time); + } else { + gossip_debug (DEBUG_DOMAIN, "DND from unknown source"); + gtk_drag_finish (context, FALSE, FALSE, time); + } +} + +static gboolean +chat_window_urgency_timeout_func (GossipChatWindow *window) +{ + GossipChatWindowPriv *priv; + + priv = GET_PRIV (window); + + gossip_debug (DEBUG_DOMAIN, "Turning off urgency hint"); + gtk_window_set_urgency_hint (GTK_WINDOW (priv->dialog), FALSE); + + priv->urgency_timeout_id = 0; + + return FALSE; +} + +static void +chat_window_set_urgency_hint (GossipChatWindow *window, + gboolean urgent) +{ + GossipChatWindowPriv *priv; + + priv = GET_PRIV (window); + + if (!urgent) { + /* Remove any existing hint and timeout. */ + if (priv->urgency_timeout_id) { + gossip_debug (DEBUG_DOMAIN, "Turning off urgency hint"); + gtk_window_set_urgency_hint (GTK_WINDOW (priv->dialog), FALSE); + g_source_remove (priv->urgency_timeout_id); + priv->urgency_timeout_id = 0; + } + return; + } + + /* Add a new hint and renew any exising timeout or add a new one. */ + if (priv->urgency_timeout_id) { + g_source_remove (priv->urgency_timeout_id); + } else { + gossip_debug (DEBUG_DOMAIN, "Turning on urgency hint"); + gtk_window_set_urgency_hint (GTK_WINDOW (priv->dialog), TRUE); + } + + priv->urgency_timeout_id = g_timeout_add ( + URGENCY_TIMEOUT, + (GSourceFunc) chat_window_urgency_timeout_func, + window); +} + +GossipChatWindow * +gossip_chat_window_new (void) +{ + return GOSSIP_CHAT_WINDOW (g_object_new (GOSSIP_TYPE_CHAT_WINDOW, NULL)); +} + +GtkWidget * +gossip_chat_window_get_dialog (GossipChatWindow *window) +{ + GossipChatWindowPriv *priv; + + g_return_val_if_fail (window != NULL, NULL); + + priv = GET_PRIV (window); + + return priv->dialog; +} + +void +gossip_chat_window_add_chat (GossipChatWindow *window, + GossipChat *chat) +{ + GossipChatWindowPriv *priv; + GtkWidget *label; + GtkWidget *child; + + priv = GET_PRIV (window); + + /* Reference the chat object */ + g_object_ref (chat); + + /* Set the chat window */ + gossip_chat_set_window (chat, window); + + if (g_list_length (priv->chats) == 0) { + gint x, y, w, h; + + gossip_chat_load_geometry (chat, &x, &y, &w, &h); + + if (x >= 0 && y >= 0) { + /* Let the window manager position it if we don't have + * good x, y coordinates. + */ + gtk_window_move (GTK_WINDOW (priv->dialog), x, y); + } + + if (w > 0 && h > 0) { + /* Use the defaults from the glade file if we don't have + * good w, h geometry. + */ + gtk_window_resize (GTK_WINDOW (priv->dialog), w, h); + } + } + + child = gossip_chat_get_widget (chat); + label = chat_window_create_label (window, chat); + + gtk_notebook_append_page (GTK_NOTEBOOK (priv->notebook), child, label); + gtk_notebook_set_tab_reorderable (GTK_NOTEBOOK (priv->notebook), child, TRUE); + gtk_notebook_set_tab_detachable (GTK_NOTEBOOK (priv->notebook), child, TRUE); + gtk_notebook_set_tab_label_packing (GTK_NOTEBOOK (priv->notebook), child, + TRUE, TRUE, GTK_PACK_START); + + gossip_debug (DEBUG_DOMAIN, + "Chat added (%d references)", + G_OBJECT (chat)->ref_count); +} + +void +gossip_chat_window_remove_chat (GossipChatWindow *window, + GossipChat *chat) +{ + GossipChatWindowPriv *priv; + gint position; + + priv = GET_PRIV (window); + + position = gtk_notebook_page_num (GTK_NOTEBOOK (priv->notebook), + gossip_chat_get_widget (chat)); + gtk_notebook_remove_page (GTK_NOTEBOOK (priv->notebook), position); + + gossip_debug (DEBUG_DOMAIN, + "Chat removed (%d references)", + G_OBJECT (chat)->ref_count - 1); + + g_object_unref (chat); +} + +void +gossip_chat_window_move_chat (GossipChatWindow *old_window, + GossipChatWindow *new_window, + GossipChat *chat) +{ + GtkWidget *widget; + + g_return_if_fail (GOSSIP_IS_CHAT_WINDOW (old_window)); + g_return_if_fail (GOSSIP_IS_CHAT_WINDOW (new_window)); + g_return_if_fail (GOSSIP_IS_CHAT (chat)); + + widget = gossip_chat_get_widget (chat); + + gossip_debug (DEBUG_DOMAIN, + "Chat moving with widget:%p (%d references)", + widget, + G_OBJECT (widget)->ref_count); + + /* We reference here to make sure we don't loose the widget + * and the GossipChat object during the move. + */ + g_object_ref (chat); + g_object_ref (widget); + + gossip_chat_window_remove_chat (old_window, chat); + gossip_chat_window_add_chat (new_window, chat); + + g_object_unref (widget); + g_object_unref (chat); +} + +void +gossip_chat_window_switch_to_chat (GossipChatWindow *window, + GossipChat *chat) +{ + GossipChatWindowPriv *priv; + gint page_num; + + priv = GET_PRIV (window); + + page_num = gtk_notebook_page_num (GTK_NOTEBOOK (priv->notebook), + gossip_chat_get_widget (chat)); + gtk_notebook_set_current_page (GTK_NOTEBOOK (priv->notebook), + page_num); +} + +gboolean +gossip_chat_window_has_focus (GossipChatWindow *window) +{ + GossipChatWindowPriv *priv; + gboolean has_focus; + + g_return_val_if_fail (GOSSIP_IS_CHAT_WINDOW (window), FALSE); + + priv = GET_PRIV (window); + + g_object_get (priv->dialog, "has-toplevel-focus", &has_focus, NULL); + + return has_focus; +} diff --git a/libempathy-gtk/gossip-chat-window.h b/libempathy-gtk/gossip-chat-window.h new file mode 100644 index 00000000..8cf582d8 --- /dev/null +++ b/libempathy-gtk/gossip-chat-window.h @@ -0,0 +1,74 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2003-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Mikael Hallendal <micke@imendio.com> + * Richard Hult <richard@imendio.com> + * Martyn Russell <martyn@imendio.com> + * Geert-Jan Van den Bogaerde <geertjan@gnome.org> + */ + +#ifndef __GOSSIP_CHAT_WINDOW_H__ +#define __GOSSIP_CHAT_WINDOW_H__ + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GOSSIP_TYPE_CHAT_WINDOW (gossip_chat_window_get_type ()) +#define GOSSIP_CHAT_WINDOW(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GOSSIP_TYPE_CHAT_WINDOW, GossipChatWindow)) +#define GOSSIP_CHAT_WINDOW_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), GOSSIP_TYPE_CHAT_WINDOW, GossipChatWindowClass)) +#define GOSSIP_IS_CHAT_WINDOW(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GOSSIP_TYPE_CHAT_WINDOW)) +#define GOSSIP_IS_CHAT_WINDOW_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GOSSIP_TYPE_CHAT_WINDOW)) +#define GOSSIP_CHAT_WINDOW_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GOSSIP_TYPE_CHAT_WINDOW, GossipChatWindowClass)) + +typedef struct _GossipChatWindow GossipChatWindow; +typedef struct _GossipChatWindowClass GossipChatWindowClass; +typedef struct _GossipChatWindowPriv GossipChatWindowPriv; + +#include "gossip-chat.h" + +struct _GossipChatWindow { + GObject parent; +}; + +struct _GossipChatWindowClass { + GObjectClass parent_class; +}; + +GType gossip_chat_window_get_type (void); +GossipChatWindow *gossip_chat_window_get_default (void); + +GossipChatWindow *gossip_chat_window_new (void); + +GtkWidget * gossip_chat_window_get_dialog (GossipChatWindow *window); + +void gossip_chat_window_add_chat (GossipChatWindow *window, + GossipChat *chat); +void gossip_chat_window_remove_chat (GossipChatWindow *window, + GossipChat *chat); +void gossip_chat_window_move_chat (GossipChatWindow *old_window, + GossipChatWindow *new_window, + GossipChat *chat); +void gossip_chat_window_switch_to_chat (GossipChatWindow *window, + GossipChat *chat); +gboolean gossip_chat_window_has_focus (GossipChatWindow *window); + +G_END_DECLS + +#endif /* __GOSSIP_CHAT_WINDOW_H__ */ diff --git a/libempathy-gtk/gossip-chat.c b/libempathy-gtk/gossip-chat.c new file mode 100644 index 00000000..616b3abe --- /dev/null +++ b/libempathy-gtk/gossip-chat.c @@ -0,0 +1,1295 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2002-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Mikael Hallendal <micke@imendio.com> + * Richard Hult <richard@imendio.com> + * Martyn Russell <martyn@imendio.com> + * Geert-Jan Van den Bogaerde <geertjan@gnome.org> + */ + +#include "config.h" + +#include <string.h> +#include <stdlib.h> + +#include <gdk/gdkkeysyms.h> +#include <glib/gi18n.h> +#include <gtk/gtk.h> +#include <glade/glade.h> + +#include <libempathy/empathy-session.h> +#include <libempathy/empathy-contact-manager.h> +#include <libempathy/gossip-debug.h> +#include <libempathy/gossip-utils.h> +#include <libempathy/gossip-conf.h> + +#include "gossip-chat.h" +#include "gossip-chat-window.h" +//#include "gossip-geometry.h" +#include "gossip-preferences.h" +#include "gossip-spell.h" +//#include "gossip-spell-dialog.h" +#include "gossip-ui-utils.h" + +#define GET_PRIV(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GOSSIP_TYPE_CHAT, GossipChatPriv)) + +#define DEBUG_DOMAIN "Chat" + +#define CHAT_DIR_CREATE_MODE (S_IRUSR | S_IWUSR | S_IXUSR) +#define CHAT_FILE_CREATE_MODE (S_IRUSR | S_IWUSR) + +#define IS_ENTER(v) (v == GDK_Return || v == GDK_ISO_Enter || v == GDK_KP_Enter) + +#define MAX_INPUT_HEIGHT 150 + +#define COMPOSING_STOP_TIMEOUT 5 + +struct _GossipChatPriv { + EmpathyTpChat *tp_chat; + GossipChatWindow *window; + + GtkTooltips *tooltips; + guint composing_stop_timeout_id; + gboolean sensitive; + /* Used to automatically shrink a window that has temporarily + * grown due to long input. + */ + gint padding_height; + gint default_window_height; + gint last_input_height; + gboolean vscroll_visible; +}; + +typedef struct { + GossipChat *chat; + gchar *word; + + GtkTextIter start; + GtkTextIter end; +} GossipChatSpell; + +static void gossip_chat_class_init (GossipChatClass *klass); +static void gossip_chat_init (GossipChat *chat); +static void chat_finalize (GObject *object); +static void chat_destroy_cb (EmpathyTpChat *tp_chat, + GossipChat *chat); +static void chat_send (GossipChat *chat, + const gchar *msg); +static void chat_input_text_view_send (GossipChat *chat); +static void chat_message_received_cb (EmpathyTpChat *tp_chat, + GossipMessage *message, + GossipChat *chat); +static gboolean chat_input_key_press_event_cb (GtkWidget *widget, + GdkEventKey *event, + GossipChat *chat); +static void chat_input_text_buffer_changed_cb (GtkTextBuffer *buffer, + GossipChat *chat); +static gboolean chat_text_view_focus_in_event_cb (GtkWidget *widget, + GdkEvent *event, + GossipChat *chat); +static void chat_text_view_scroll_hide_cb (GtkWidget *widget, + GossipChat *chat); +static void chat_text_view_size_allocate_cb (GtkWidget *widget, + GtkAllocation *allocation, + GossipChat *chat); +static void chat_text_view_realize_cb (GtkWidget *widget, + GossipChat *chat); +static void chat_text_populate_popup_cb (GtkTextView *view, + GtkMenu *menu, + GossipChat *chat); +static void chat_text_check_word_spelling_cb (GtkMenuItem *menuitem, + GossipChatSpell *chat_spell); +static GossipChatSpell *chat_spell_new (GossipChat *chat, + const gchar *word, + GtkTextIter start, + GtkTextIter end); +static void chat_spell_free (GossipChatSpell *chat_spell); +static void chat_composing_start (GossipChat *chat); +static void chat_composing_stop (GossipChat *chat); +static void chat_composing_remove_timeout (GossipChat *chat); +static gboolean chat_composing_stop_timeout_cb (GossipChat *chat); + +enum { + COMPOSING, + NEW_MESSAGE, + NAME_CHANGED, + STATUS_CHANGED, + LAST_SIGNAL +}; + +static guint chat_signals[LAST_SIGNAL] = { 0 }; + +G_DEFINE_TYPE (GossipChat, gossip_chat, G_TYPE_OBJECT); + +static void +gossip_chat_class_init (GossipChatClass *klass) +{ + GObjectClass *object_class; + + object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = chat_finalize; + + chat_signals[COMPOSING] = + g_signal_new ("composing", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__BOOLEAN, + G_TYPE_NONE, + 1, G_TYPE_BOOLEAN); + + chat_signals[NEW_MESSAGE] = + g_signal_new ("new-message", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, + 1, GOSSIP_TYPE_MESSAGE); + + chat_signals[NAME_CHANGED] = + g_signal_new ("name-changed", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__POINTER, + G_TYPE_NONE, + 1, G_TYPE_POINTER); + + chat_signals[STATUS_CHANGED] = + g_signal_new ("status-changed", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); + + g_type_class_add_private (object_class, sizeof (GossipChatPriv)); +} + +static void +gossip_chat_init (GossipChat *chat) +{ + GossipChatPriv *priv; + GtkTextBuffer *buffer; + + chat->view = gossip_chat_view_new (); + chat->input_text_view = gtk_text_view_new (); + + chat->is_first_char = TRUE; + + g_object_set (chat->input_text_view, + "pixels-above-lines", 2, + "pixels-below-lines", 2, + "pixels-inside-wrap", 1, + "right-margin", 2, + "left-margin", 2, + "wrap-mode", GTK_WRAP_WORD_CHAR, + NULL); + + priv = GET_PRIV (chat); + + priv->tooltips = gtk_tooltips_new (); + + priv->default_window_height = -1; + priv->vscroll_visible = FALSE; + priv->sensitive = TRUE; + + g_signal_connect (chat->input_text_view, + "key_press_event", + G_CALLBACK (chat_input_key_press_event_cb), + chat); + + buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view)); + g_signal_connect (buffer, + "changed", + G_CALLBACK (chat_input_text_buffer_changed_cb), + chat); + g_signal_connect (GOSSIP_CHAT (chat)->view, + "focus_in_event", + G_CALLBACK (chat_text_view_focus_in_event_cb), + chat); + + g_signal_connect (chat->input_text_view, + "size_allocate", + G_CALLBACK (chat_text_view_size_allocate_cb), + chat); + + g_signal_connect (chat->input_text_view, + "realize", + G_CALLBACK (chat_text_view_realize_cb), + chat); + + g_signal_connect (GTK_TEXT_VIEW (chat->input_text_view), + "populate_popup", + G_CALLBACK (chat_text_populate_popup_cb), + chat); + + /* create misspelt words identification tag */ + gtk_text_buffer_create_tag (buffer, + "misspelled", + "underline", PANGO_UNDERLINE_ERROR, + NULL); +} + +static void +chat_finalize (GObject *object) +{ + GossipChat *chat; + GossipChatPriv *priv; + + chat = GOSSIP_CHAT (object); + priv = GET_PRIV (chat); + + gossip_debug (DEBUG_DOMAIN, "Finalized: %p", object); + + chat_composing_remove_timeout (chat); + g_object_unref (GOSSIP_CHAT (object)->account); + + if (priv->tp_chat) { + g_object_unref (priv->tp_chat); + } + + G_OBJECT_CLASS (gossip_chat_parent_class)->finalize (object); +} + +static void +chat_destroy_cb (EmpathyTpChat *tp_chat, + GossipChat *chat) +{ + GossipChatPriv *priv; + GtkWidget *widget; + + priv = GET_PRIV (chat); + + if (priv->tp_chat) { + g_object_unref (priv->tp_chat); + priv->tp_chat = NULL; + } + + gossip_chat_view_append_event (chat->view, _("Disconnected")); + + widget = gossip_chat_get_widget (chat); + gtk_widget_set_sensitive (widget, FALSE); + priv->sensitive = FALSE; +} + +static void +chat_send (GossipChat *chat, + const gchar *msg) +{ + GossipChatPriv *priv; + //GossipLogManager *log_manager; + GossipMessage *message; + GossipContact *own_contact; + + priv = GET_PRIV (chat); + + if (msg == NULL || msg[0] == '\0') { + return; + } + + if (g_str_has_prefix (msg, "/clear")) { + gossip_chat_view_clear (chat->view); + return; + } + + /* FIXME: gossip_app_set_not_away ();*/ + + own_contact = gossip_chat_get_own_contact (chat); + message = gossip_message_new (msg); + gossip_message_set_sender (message, own_contact); + + //FIXME: log_manager = gossip_session_get_log_manager (gossip_app_get_session ()); + //gossip_log_message_for_contact (log_manager, message, FALSE); + + empathy_tp_chat_send (priv->tp_chat, message); + + g_object_unref (message); +} + +static void +chat_input_text_view_send (GossipChat *chat) +{ + GossipChatPriv *priv; + GtkTextBuffer *buffer; + GtkTextIter start, end; + gchar *msg; + + priv = GET_PRIV (chat); + + buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view)); + + gtk_text_buffer_get_bounds (buffer, &start, &end); + msg = gtk_text_buffer_get_text (buffer, &start, &end, FALSE); + + /* clear the input field */ + gtk_text_buffer_set_text (buffer, "", -1); + + chat_send (chat, msg); + + g_free (msg); + + chat->is_first_char = TRUE; +} + +static void +chat_message_received_cb (EmpathyTpChat *tp_chat, + GossipMessage *message, + GossipChat *chat) +{ + GossipChatPriv *priv; + //GossipLogManager *log_manager; + GossipContact *sender; + + priv = GET_PRIV (chat); + + sender = gossip_message_get_sender (message); + gossip_debug (DEBUG_DOMAIN, "Appending message ('%s')", + gossip_contact_get_name (sender)); + +/*FIXME: + log_manager = gossip_session_get_log_manager (gossip_app_get_session ()); + gossip_log_message_for_contact (log_manager, message, TRUE); +*/ + gossip_chat_view_append_message (chat->view, message); + + if (gossip_chat_should_play_sound (chat)) { + // FIXME: gossip_sound_play (GOSSIP_SOUND_CHAT); + } + + g_signal_emit_by_name (chat, "new-message", message); +} + +static gboolean +chat_input_key_press_event_cb (GtkWidget *widget, + GdkEventKey *event, + GossipChat *chat) +{ + GossipChatPriv *priv; + GtkAdjustment *adj; + gdouble val; + GtkWidget *text_view_sw; + + priv = GET_PRIV (chat); + + if (event->keyval == GDK_Tab && !(event->state & GDK_CONTROL_MASK)) { + return TRUE; + } + + /* Catch enter but not ctrl/shift-enter */ + if (IS_ENTER (event->keyval) && !(event->state & GDK_SHIFT_MASK)) { + GtkTextView *view; + + /* This is to make sure that kinput2 gets the enter. And if + * it's handled there we shouldn't send on it. This is because + * kinput2 uses Enter to commit letters. See: + * http://bugzilla.redhat.com/bugzilla/show_bug.cgi?id=104299 + */ + + view = GTK_TEXT_VIEW (chat->input_text_view); + if (gtk_im_context_filter_keypress (view->im_context, event)) { + GTK_TEXT_VIEW (chat->input_text_view)->need_im_reset = TRUE; + return TRUE; + } + + chat_input_text_view_send (chat); + return TRUE; + } + + text_view_sw = gtk_widget_get_parent (GTK_WIDGET (chat->view)); + if (IS_ENTER (event->keyval) && (event->state & GDK_SHIFT_MASK)) { + /* Newline for shift-enter. */ + return FALSE; + } + else if ((event->state & GDK_CONTROL_MASK) != GDK_CONTROL_MASK && + event->keyval == GDK_Page_Up) { + adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (text_view_sw)); + gtk_adjustment_set_value (adj, adj->value - adj->page_size); + + return TRUE; + } + else if ((event->state & GDK_CONTROL_MASK) != GDK_CONTROL_MASK && + event->keyval == GDK_Page_Down) { + adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (text_view_sw)); + val = MIN (adj->value + adj->page_size, adj->upper - adj->page_size); + gtk_adjustment_set_value (adj, val); + + return TRUE; + } + + return FALSE; +} + +static gboolean +chat_text_view_focus_in_event_cb (GtkWidget *widget, + GdkEvent *event, + GossipChat *chat) +{ + gtk_widget_grab_focus (chat->input_text_view); + + return TRUE; +} + +static void +chat_input_text_buffer_changed_cb (GtkTextBuffer *buffer, + GossipChat *chat) +{ + GossipChatPriv *priv; + GtkTextIter start, end; + gchar *str; + gboolean spell_checker = FALSE; + + priv = GET_PRIV (chat); + + if (gtk_text_buffer_get_char_count (buffer) == 0) { + chat_composing_stop (chat); + } else { + chat_composing_start (chat); + } + + gossip_conf_get_bool (gossip_conf_get (), + GOSSIP_PREFS_CHAT_SPELL_CHECKER_ENABLED, + &spell_checker); + + if (chat->is_first_char) { + GtkRequisition req; + gint window_height; + GtkWidget *dialog; + GtkAllocation *allocation; + + /* Save the window's size */ + dialog = gossip_chat_window_get_dialog (priv->window); + gtk_window_get_size (GTK_WINDOW (dialog), + NULL, &window_height); + + gtk_widget_size_request (chat->input_text_view, &req); + + allocation = >K_WIDGET (chat->view)->allocation; + + priv->default_window_height = window_height; + priv->last_input_height = req.height; + priv->padding_height = window_height - req.height - allocation->height; + + chat->is_first_char = FALSE; + } + + gtk_text_buffer_get_start_iter (buffer, &start); + + if (!spell_checker) { + gtk_text_buffer_get_end_iter (buffer, &end); + gtk_text_buffer_remove_tag_by_name (buffer, "misspelled", &start, &end); + return; + } + + if (!gossip_spell_supported ()) { + return; + } + + /* NOTE: this is really inefficient, we shouldn't have to + reiterate the whole buffer each time and check each work + every time. */ + while (TRUE) { + gboolean correct = FALSE; + + /* if at start */ + if (gtk_text_iter_is_start (&start)) { + end = start; + + if (!gtk_text_iter_forward_word_end (&end)) { + /* no whole word yet */ + break; + } + } else { + if (!gtk_text_iter_forward_word_end (&end)) { + /* must be the end of the buffer */ + break; + } + + start = end; + gtk_text_iter_backward_word_start (&start); + } + + str = gtk_text_buffer_get_text (buffer, &start, &end, FALSE); + + /* spell check string */ + if (!gossip_chat_get_is_command (str)) { + correct = gossip_spell_check (str); + } else { + correct = TRUE; + } + + if (!correct) { + gtk_text_buffer_apply_tag_by_name (buffer, "misspelled", &start, &end); + } else { + gtk_text_buffer_remove_tag_by_name (buffer, "misspelled", &start, &end); + } + + g_free (str); + + /* set start iter to the end iters position */ + start = end; + } +} + +typedef struct { + GtkWidget *window; + gint width; + gint height; +} ChangeSizeData; + +static gboolean +chat_change_size_in_idle_cb (ChangeSizeData *data) +{ + gtk_window_resize (GTK_WINDOW (data->window), + data->width, data->height); + + return FALSE; +} + +static void +chat_text_view_scroll_hide_cb (GtkWidget *widget, + GossipChat *chat) +{ + GossipChatPriv *priv; + GtkWidget *sw; + + priv = GET_PRIV (chat); + + priv->vscroll_visible = FALSE; + g_signal_handlers_disconnect_by_func (widget, chat_text_view_scroll_hide_cb, chat); + + sw = gtk_widget_get_parent (chat->input_text_view); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw), + GTK_POLICY_NEVER, + GTK_POLICY_NEVER); + g_object_set (sw, "height-request", -1, NULL); +} + +static void +chat_text_view_size_allocate_cb (GtkWidget *widget, + GtkAllocation *allocation, + GossipChat *chat) +{ + GossipChatPriv *priv; + gint width; + GtkWidget *dialog; + ChangeSizeData *data; + gint window_height; + gint new_height; + GtkAllocation *view_allocation; + gint current_height; + gint diff; + GtkWidget *sw; + + priv = GET_PRIV (chat); + + if (priv->default_window_height <= 0) { + return; + } + + sw = gtk_widget_get_parent (widget); + if (sw->allocation.height >= MAX_INPUT_HEIGHT && !priv->vscroll_visible) { + GtkWidget *vscroll; + + priv->vscroll_visible = TRUE; + gtk_widget_set_size_request (sw, sw->allocation.width, MAX_INPUT_HEIGHT); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw), + GTK_POLICY_NEVER, + GTK_POLICY_AUTOMATIC); + vscroll = gtk_scrolled_window_get_vscrollbar (GTK_SCROLLED_WINDOW (sw)); + g_signal_connect (vscroll, "hide", + G_CALLBACK (chat_text_view_scroll_hide_cb), + chat); + } + + if (priv->last_input_height <= allocation->height) { + priv->last_input_height = allocation->height; + return; + } + + diff = priv->last_input_height - allocation->height; + priv->last_input_height = allocation->height; + + view_allocation = >K_WIDGET (chat->view)->allocation; + + dialog = gossip_chat_window_get_dialog (priv->window); + gtk_window_get_size (GTK_WINDOW (dialog), NULL, ¤t_height); + + new_height = view_allocation->height + priv->padding_height + allocation->height - diff; + + if (new_height <= priv->default_window_height) { + window_height = priv->default_window_height; + } else { + window_height = new_height; + } + + if (current_height <= window_height) { + return; + } + + /* Restore the window's size */ + gtk_window_get_size (GTK_WINDOW (dialog), &width, NULL); + + data = g_new0 (ChangeSizeData, 1); + data->window = dialog; + data->width = width; + data->height = window_height; + + g_idle_add_full (G_PRIORITY_DEFAULT_IDLE, + (GSourceFunc) chat_change_size_in_idle_cb, + data, g_free); +} + +static void +chat_text_view_realize_cb (GtkWidget *widget, + GossipChat *chat) +{ + gossip_debug (DEBUG_DOMAIN, "Setting focus to the input text view"); + gtk_widget_grab_focus (widget); +} + +static void +chat_insert_smiley_activate_cb (GtkWidget *menuitem, + GossipChat *chat) +{ + GtkTextBuffer *buffer; + GtkTextIter iter; + const gchar *smiley; + + smiley = g_object_get_data (G_OBJECT (menuitem), "smiley_text"); + + buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view)); + + gtk_text_buffer_get_end_iter (buffer, &iter); + gtk_text_buffer_insert (buffer, &iter, smiley, -1); + + gtk_text_buffer_get_end_iter (buffer, &iter); + gtk_text_buffer_insert (buffer, &iter, " ", -1); +} + +static void +chat_text_populate_popup_cb (GtkTextView *view, + GtkMenu *menu, + GossipChat *chat) +{ + GossipChatPriv *priv; + GtkTextBuffer *buffer; + GtkTextTagTable *table; + GtkTextTag *tag; + gint x, y; + GtkTextIter iter, start, end; + GtkWidget *item; + gchar *str = NULL; + GossipChatSpell *chat_spell; + GtkWidget *smiley_menu; + + priv = GET_PRIV (chat); + + /* Add the emoticon menu. */ + item = gtk_separator_menu_item_new (); + gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item); + gtk_widget_show (item); + + item = gtk_menu_item_new_with_mnemonic (_("Insert Smiley")); + gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item); + gtk_widget_show (item); + + smiley_menu = gossip_chat_view_get_smiley_menu ( + G_CALLBACK (chat_insert_smiley_activate_cb), + chat, + priv->tooltips); + gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), smiley_menu); + + /* Add the spell check menu item. */ + buffer = gtk_text_view_get_buffer (view); + table = gtk_text_buffer_get_tag_table (buffer); + + tag = gtk_text_tag_table_lookup (table, "misspelled"); + + gtk_widget_get_pointer (GTK_WIDGET (view), &x, &y); + + gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (view), + GTK_TEXT_WINDOW_WIDGET, + x, y, + &x, &y); + + gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (view), &iter, x, y); + + start = end = iter; + + if (gtk_text_iter_backward_to_tag_toggle (&start, tag) && + gtk_text_iter_forward_to_tag_toggle (&end, tag)) { + + str = gtk_text_buffer_get_text (buffer, + &start, &end, FALSE); + } + + if (G_STR_EMPTY (str)) { + return; + } + + chat_spell = chat_spell_new (chat, str, start, end); + + g_object_set_data_full (G_OBJECT (menu), + "chat_spell", chat_spell, + (GDestroyNotify) chat_spell_free); + + item = gtk_separator_menu_item_new (); + gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item); + gtk_widget_show (item); + + item = gtk_menu_item_new_with_mnemonic (_("_Check Word Spelling...")); + g_signal_connect (item, + "activate", + G_CALLBACK (chat_text_check_word_spelling_cb), + chat_spell); + gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item); + gtk_widget_show (item); +} + +static void +chat_text_check_word_spelling_cb (GtkMenuItem *menuitem, + GossipChatSpell *chat_spell) +{ +/*FIXME: gossip_spell_dialog_show (chat_spell->chat, + chat_spell->start, + chat_spell->end, + chat_spell->word);*/ +} + +static GossipChatSpell * +chat_spell_new (GossipChat *chat, + const gchar *word, + GtkTextIter start, + GtkTextIter end) +{ + GossipChatSpell *chat_spell; + + chat_spell = g_new0 (GossipChatSpell, 1); + + chat_spell->chat = g_object_ref (chat); + chat_spell->word = g_strdup (word); + chat_spell->start = start; + chat_spell->end = end; + + return chat_spell; +} + +static void +chat_spell_free (GossipChatSpell *chat_spell) +{ + g_object_unref (chat_spell->chat); + g_free (chat_spell->word); + g_free (chat_spell); +} + +static void +chat_composing_start (GossipChat *chat) +{ + GossipChatPriv *priv; + + priv = GET_PRIV (chat); + + if (priv->composing_stop_timeout_id) { + /* Just restart the timeout */ + chat_composing_remove_timeout (chat); + } else { + /* FIXME: + gossip_session_send_composing (gossip_app_get_session (), + priv->contact, TRUE); + */ + } + + priv->composing_stop_timeout_id = g_timeout_add ( + 1000 * COMPOSING_STOP_TIMEOUT, + (GSourceFunc) chat_composing_stop_timeout_cb, + chat); +} + +static void +chat_composing_stop (GossipChat *chat) +{ + GossipChatPriv *priv; + + priv = GET_PRIV (chat); + + chat_composing_remove_timeout (chat); + /* FIXME: + gossip_session_send_composing (gossip_app_get_session (), + priv->contact, FALSE);*/ +} + +static void +chat_composing_remove_timeout (GossipChat *chat) +{ + GossipChatPriv *priv; + + priv = GET_PRIV (chat); + + if (priv->composing_stop_timeout_id) { + g_source_remove (priv->composing_stop_timeout_id); + priv->composing_stop_timeout_id = 0; + } +} + +static gboolean +chat_composing_stop_timeout_cb (GossipChat *chat) +{ + GossipChatPriv *priv; + + priv = GET_PRIV (chat); + + priv->composing_stop_timeout_id = 0; + /* FIXME: + gossip_session_send_composing (gossip_app_get_session (), + priv->contact, FALSE);*/ + + return FALSE; +} + +gboolean +gossip_chat_get_is_command (const gchar *str) +{ + g_return_val_if_fail (str != NULL, FALSE); + + if (str[0] != '/') { + return FALSE; + } + + if (g_str_has_prefix (str, "/me")) { + return TRUE; + } + else if (g_str_has_prefix (str, "/nick")) { + return TRUE; + } + else if (g_str_has_prefix (str, "/topic")) { + return TRUE; + } + + return FALSE; +} + +void +gossip_chat_correct_word (GossipChat *chat, + GtkTextIter start, + GtkTextIter end, + const gchar *new_word) +{ + GtkTextBuffer *buffer; + + g_return_if_fail (chat != NULL); + g_return_if_fail (new_word != NULL); + + buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view)); + + gtk_text_buffer_delete (buffer, &start, &end); + gtk_text_buffer_insert (buffer, &start, + new_word, + -1); +} + +const gchar * +gossip_chat_get_name (GossipChat *chat) +{ + g_return_val_if_fail (GOSSIP_IS_CHAT (chat), NULL); + + if (GOSSIP_CHAT_GET_CLASS (chat)->get_name) { + return GOSSIP_CHAT_GET_CLASS (chat)->get_name (chat); + } + + return NULL; +} + +gchar * +gossip_chat_get_tooltip (GossipChat *chat) +{ + g_return_val_if_fail (GOSSIP_IS_CHAT (chat), NULL); + + if (GOSSIP_CHAT_GET_CLASS (chat)->get_tooltip) { + return GOSSIP_CHAT_GET_CLASS (chat)->get_tooltip (chat); + } + + return NULL; +} + +GdkPixbuf * +gossip_chat_get_status_pixbuf (GossipChat *chat) +{ + g_return_val_if_fail (GOSSIP_IS_CHAT (chat), NULL); + + if (GOSSIP_CHAT_GET_CLASS (chat)->get_status_pixbuf) { + return GOSSIP_CHAT_GET_CLASS (chat)->get_status_pixbuf (chat); + } + + return NULL; +} + +GossipContact * +gossip_chat_get_contact (GossipChat *chat) +{ + g_return_val_if_fail (GOSSIP_IS_CHAT (chat), NULL); + + if (GOSSIP_CHAT_GET_CLASS (chat)->get_contact) { + return GOSSIP_CHAT_GET_CLASS (chat)->get_contact (chat); + } + + return NULL; +} +GossipContact * +gossip_chat_get_own_contact (GossipChat *chat) +{ + EmpathyContactManager *manager; + + g_return_val_if_fail (GOSSIP_IS_CHAT (chat), NULL); + + manager = empathy_session_get_contact_manager (); + + return empathy_contact_manager_get_own (manager, chat->account); +} + +GtkWidget * +gossip_chat_get_widget (GossipChat *chat) +{ + g_return_val_if_fail (GOSSIP_IS_CHAT (chat), NULL); + + if (GOSSIP_CHAT_GET_CLASS (chat)->get_widget) { + return GOSSIP_CHAT_GET_CLASS (chat)->get_widget (chat); + } + + return NULL; +} + +gboolean +gossip_chat_is_group_chat (GossipChat *chat) +{ + g_return_val_if_fail (GOSSIP_IS_CHAT (chat), FALSE); + + if (GOSSIP_CHAT_GET_CLASS (chat)->is_group_chat) { + return GOSSIP_CHAT_GET_CLASS (chat)->is_group_chat (chat); + } + + return FALSE; +} + +gboolean +gossip_chat_is_connected (GossipChat *chat) +{ + GossipChatPriv *priv; + + g_return_val_if_fail (GOSSIP_IS_CHAT (chat), FALSE); + + priv = GET_PRIV (chat); + + return (priv->tp_chat != NULL); +} + +gboolean +gossip_chat_get_show_contacts (GossipChat *chat) +{ + g_return_val_if_fail (GOSSIP_IS_CHAT (chat), FALSE); + + if (GOSSIP_CHAT_GET_CLASS (chat)->get_show_contacts) { + return GOSSIP_CHAT_GET_CLASS (chat)->get_show_contacts (chat); + } + + return FALSE; +} + +void +gossip_chat_set_show_contacts (GossipChat *chat, + gboolean show) +{ + g_return_if_fail (GOSSIP_IS_CHAT (chat)); + + if (GOSSIP_CHAT_GET_CLASS (chat)->set_show_contacts) { + GOSSIP_CHAT_GET_CLASS (chat)->set_show_contacts (chat, show); + } +} + +void +gossip_chat_save_geometry (GossipChat *chat, + gint x, + gint y, + gint w, + gint h) +{ + //FIXME: gossip_geometry_save_for_chat (chat, x, y, w, h); +} + +void +gossip_chat_load_geometry (GossipChat *chat, + gint *x, + gint *y, + gint *w, + gint *h) +{ + //FIXME: gossip_geometry_load_for_chat (chat, x, y, w, h); +} + +void +gossip_chat_set_tp_chat (GossipChat *chat, + EmpathyTpChat *tp_chat) +{ + GossipChatPriv *priv; + GtkWidget *widget; + + g_return_if_fail (GOSSIP_IS_CHAT (chat)); + g_return_if_fail (EMPATHY_IS_TP_CHAT (tp_chat)); + + priv = GET_PRIV (chat); + + if (tp_chat == priv->tp_chat) { + return; + } + + if (priv->tp_chat) { + g_signal_handlers_disconnect_by_func (priv->tp_chat, + chat_message_received_cb, + chat); + g_signal_handlers_disconnect_by_func (priv->tp_chat, + chat_destroy_cb, + chat); + g_object_unref (priv->tp_chat); + } + + priv->tp_chat = g_object_ref (tp_chat); + + g_signal_connect (tp_chat, "message-received", + G_CALLBACK (chat_message_received_cb), + chat); + g_signal_connect (tp_chat, "destroy", + G_CALLBACK (chat_destroy_cb), + chat); + + empathy_tp_chat_request_pending (tp_chat); + + if (!priv->sensitive) { + widget = gossip_chat_get_widget (chat); + gtk_widget_set_sensitive (widget, TRUE); + gossip_chat_view_append_event (chat->view, _("Connected")); + priv->sensitive = TRUE; + } +} + +void +gossip_chat_clear (GossipChat *chat) +{ + g_return_if_fail (GOSSIP_IS_CHAT (chat)); + + gossip_chat_view_clear (chat->view); +} + +void +gossip_chat_set_window (GossipChat *chat, + GossipChatWindow *window) +{ + GossipChatPriv *priv; + + priv = GET_PRIV (chat); + priv->window = window; +} + +GossipChatWindow * +gossip_chat_get_window (GossipChat *chat) +{ + GossipChatPriv *priv; + + priv = GET_PRIV (chat); + + return priv->window; +} + +void +gossip_chat_scroll_down (GossipChat *chat) +{ + g_return_if_fail (GOSSIP_IS_CHAT (chat)); + + gossip_chat_view_scroll_down (chat->view); +} + +void +gossip_chat_cut (GossipChat *chat) +{ + GtkTextBuffer *buffer; + + g_return_if_fail (GOSSIP_IS_CHAT (chat)); + + buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view)); + if (gtk_text_buffer_get_selection_bounds (buffer, NULL, NULL)) { + GtkClipboard *clipboard; + + clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD); + + gtk_text_buffer_cut_clipboard (buffer, clipboard, TRUE); + } +} + +void +gossip_chat_copy (GossipChat *chat) +{ + GtkTextBuffer *buffer; + + g_return_if_fail (GOSSIP_IS_CHAT (chat)); + + if (gossip_chat_view_get_selection_bounds (chat->view, NULL, NULL)) { + gossip_chat_view_copy_clipboard (chat->view); + return; + } + + buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view)); + if (gtk_text_buffer_get_selection_bounds (buffer, NULL, NULL)) { + GtkClipboard *clipboard; + + clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD); + + gtk_text_buffer_copy_clipboard (buffer, clipboard); + } +} + +void +gossip_chat_paste (GossipChat *chat) +{ + GtkTextBuffer *buffer; + GtkClipboard *clipboard; + + g_return_if_fail (GOSSIP_IS_CHAT (chat)); + + buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view)); + clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD); + + gtk_text_buffer_paste_clipboard (buffer, clipboard, NULL, TRUE); +} + +void +gossip_chat_present (GossipChat *chat) +{ + GossipChatPriv *priv; + + g_return_if_fail (GOSSIP_IS_CHAT (chat)); + + priv = GET_PRIV (chat); + + if (priv->window == NULL) { + GossipChatWindow *window; + + window = gossip_chat_window_get_default (); + if (!window) { + window = gossip_chat_window_new (); + } + + gossip_chat_window_add_chat (window, chat); + } + + gossip_chat_window_switch_to_chat (priv->window, chat); + gossip_window_present ( + GTK_WINDOW (gossip_chat_window_get_dialog (priv->window)), + TRUE); + + gtk_widget_grab_focus (chat->input_text_view); +} + +gboolean +gossip_chat_should_play_sound (GossipChat *chat) +{ + GossipChatWindow *window; + gboolean play = TRUE; + + g_return_val_if_fail (GOSSIP_IS_CHAT (chat), FALSE); + + window = gossip_chat_get_window (GOSSIP_CHAT (chat)); + if (!window) { + return TRUE; + } + + play = !gossip_chat_window_has_focus (window); + + return play; +} + +gboolean +gossip_chat_should_highlight_nick (GossipMessage *message) +{ + GossipContact *my_contact; + const gchar *msg, *to; + gchar *cf_msg, *cf_to; + gchar *ch; + gboolean ret_val; + + g_return_val_if_fail (GOSSIP_IS_MESSAGE (message), FALSE); + + gossip_debug (DEBUG_DOMAIN, "Highlighting nickname"); + + ret_val = FALSE; + + msg = gossip_message_get_body (message); + if (!msg) { + return FALSE; + } + + my_contact = gossip_get_own_contact_from_contact (gossip_message_get_sender (message)); + to = gossip_contact_get_name (my_contact); + if (!to) { + return FALSE; + } + + cf_msg = g_utf8_casefold (msg, -1); + cf_to = g_utf8_casefold (to, -1); + + ch = strstr (cf_msg, cf_to); + if (ch == NULL) { + goto finished; + } + + if (ch != cf_msg) { + /* Not first in the message */ + if ((*(ch - 1) != ' ') && + (*(ch - 1) != ',') && + (*(ch - 1) != '.')) { + goto finished; + } + } + + ch = ch + strlen (cf_to); + if (ch >= cf_msg + strlen (cf_msg)) { + ret_val = TRUE; + goto finished; + } + + if ((*ch == ' ') || + (*ch == ',') || + (*ch == '.') || + (*ch == ':')) { + ret_val = TRUE; + goto finished; + } + +finished: + g_free (cf_msg); + g_free (cf_to); + + return ret_val; +} + diff --git a/libempathy-gtk/gossip-chat.h b/libempathy-gtk/gossip-chat.h new file mode 100644 index 00000000..9a0a0c31 --- /dev/null +++ b/libempathy-gtk/gossip-chat.h @@ -0,0 +1,139 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2002-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Mikael Hallendal <micke@imendio.com> + * Richard Hult <richard@imendio.com> + * Martyn Russell <martyn@imendio.com> + * Geert-Jan Van den Bogaerde <geertjan@gnome.org> + */ + +#ifndef __GOSSIP_CHAT_H__ +#define __GOSSIP_CHAT_H__ + +#include <glib-object.h> + +#include <libempathy/gossip-contact.h> +#include <libempathy/gossip-message.h> +#include <libempathy/empathy-tp-chat.h> + +#include "gossip-chat-view.h" +#include "gossip-spell.h" + +G_BEGIN_DECLS + +#define GOSSIP_TYPE_CHAT (gossip_chat_get_type ()) +#define GOSSIP_CHAT(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GOSSIP_TYPE_CHAT, GossipChat)) +#define GOSSIP_CHAT_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), GOSSIP_TYPE_CHAT, GossipChatClass)) +#define GOSSIP_IS_CHAT(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GOSSIP_TYPE_CHAT)) +#define GOSSIP_IS_CHAT_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GOSSIP_TYPE_CHAT)) +#define GOSSIP_CHAT_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GOSSIP_TYPE_CHAT, GossipChatClass)) + +typedef struct _GossipChat GossipChat; +typedef struct _GossipChatClass GossipChatClass; +typedef struct _GossipChatPriv GossipChatPriv; + +#include "gossip-chat-window.h" + +struct _GossipChat { + GObject parent; + + /* Protected */ + GossipChatView *view; + GtkWidget *input_text_view; + gboolean is_first_char; + McAccount *account; +}; + +struct _GossipChatClass { + GObjectClass parent; + + /* VTable */ + const gchar * (*get_name) (GossipChat *chat); + gchar * (*get_tooltip) (GossipChat *chat); + GdkPixbuf * (*get_status_pixbuf)(GossipChat *chat); + GossipContact * (*get_contact) (GossipChat *chat); + GtkWidget * (*get_widget) (GossipChat *chat); + + gboolean (*get_show_contacts)(GossipChat *chat); + void (*set_show_contacts)(GossipChat *chat, + gboolean show); + + gboolean (*is_group_chat) (GossipChat *chat); + void (*save_geometry) (GossipChat *chat, + gint x, + gint y, + gint w, + gint h); + void (*load_geometry) (GossipChat *chat, + gint *x, + gint *y, + gint *w, + gint *h); +}; + +GType gossip_chat_get_type (void); + +GossipChatView * gossip_chat_get_view (GossipChat *chat); +GossipChatWindow *gossip_chat_get_window (GossipChat *chat); +void gossip_chat_set_window (GossipChat *chat, + GossipChatWindow *window); +void gossip_chat_present (GossipChat *chat); +void gossip_chat_clear (GossipChat *chat); +void gossip_chat_scroll_down (GossipChat *chat); +void gossip_chat_cut (GossipChat *chat); +void gossip_chat_copy (GossipChat *chat); +void gossip_chat_paste (GossipChat *chat); +const gchar * gossip_chat_get_name (GossipChat *chat); +gchar * gossip_chat_get_tooltip (GossipChat *chat); +GdkPixbuf * gossip_chat_get_status_pixbuf (GossipChat *chat); +GossipContact * gossip_chat_get_contact (GossipChat *chat); +GossipContact * gossip_chat_get_own_contact (GossipChat *chat); +GtkWidget * gossip_chat_get_widget (GossipChat *chat); +gboolean gossip_chat_get_show_contacts (GossipChat *chat); +void gossip_chat_set_show_contacts (GossipChat *chat, + gboolean show); + +gboolean gossip_chat_is_group_chat (GossipChat *chat); +gboolean gossip_chat_is_connected (GossipChat *chat); + +void gossip_chat_save_geometry (GossipChat *chat, + gint x, + gint y, + gint w, + gint h); +void gossip_chat_load_geometry (GossipChat *chat, + gint *x, + gint *y, + gint *w, + gint *h); +void gossip_chat_set_tp_chat (GossipChat *chat, + EmpathyTpChat *tp_chat); + +/* For spell checker dialog to correct the misspelled word. */ +gboolean gossip_chat_get_is_command (const gchar *str); +void gossip_chat_correct_word (GossipChat *chat, + GtkTextIter start, + GtkTextIter end, + const gchar *new_word); +gboolean gossip_chat_should_play_sound (GossipChat *chat); +gboolean gossip_chat_should_highlight_nick (GossipMessage *message); + +G_END_DECLS + +#endif /* __GOSSIP_CHAT_H__ */ diff --git a/libempathy-gtk/gossip-contact-groups.c b/libempathy-gtk/gossip-contact-groups.c new file mode 100644 index 00000000..8a6afda1 --- /dev/null +++ b/libempathy-gtk/gossip-contact-groups.c @@ -0,0 +1,286 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2005-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Martyn Russell <martyn@imendio.com> + */ + +#include "config.h" + +#include <string.h> +#include <sys/types.h> +#include <sys/stat.h> + +#include <glib.h> +#include <glib/gi18n.h> + +#include <libxml/parser.h> +#include <libxml/tree.h> + +#include <libempathy/gossip-debug.h> +#include <libempathy/gossip-utils.h> + +#include "gossip-contact-groups.h" + +#define DEBUG_DOMAIN "ContactGroups" + +#define CONTACT_GROUPS_XML_FILENAME "contact-groups.xml" +#define CONTACT_GROUPS_DTD_FILENAME "gossip-contact-groups.dtd" + +typedef struct { + gchar *name; + gboolean expanded; +} ContactGroup; + +static void contact_groups_file_parse (const gchar *filename); +static gboolean contact_groups_file_save (void); +static ContactGroup *contact_group_new (const gchar *name, + gboolean expanded); +static void contact_group_free (ContactGroup *group); + +static GList *groups = NULL; + +void +gossip_contact_groups_get_all (void) +{ + gchar *dir; + gchar *file_with_path; + + /* If already set up clean up first */ + if (groups) { + g_list_foreach (groups, (GFunc)contact_group_free, NULL); + g_list_free (groups); + groups = NULL; + } + + dir = g_build_filename (g_get_home_dir (), ".gnome2", PACKAGE_NAME, NULL); + file_with_path = g_build_filename (dir, CONTACT_GROUPS_XML_FILENAME, NULL); + g_free (dir); + + if (g_file_test (file_with_path, G_FILE_TEST_EXISTS)) { + contact_groups_file_parse (file_with_path); + } + + g_free (file_with_path); +} + +static void +contact_groups_file_parse (const gchar *filename) +{ + xmlParserCtxtPtr ctxt; + xmlDocPtr doc; + xmlNodePtr contacts; + xmlNodePtr account; + xmlNodePtr node; + + gossip_debug (DEBUG_DOMAIN, "Attempting to parse file:'%s'...", filename); + + ctxt = xmlNewParserCtxt (); + + /* Parse and validate the file. */ + doc = xmlCtxtReadFile (ctxt, filename, NULL, 0); + if (!doc) { + g_warning ("Failed to parse file:'%s'", filename); + xmlFreeParserCtxt (ctxt); + return; + } + + if (!gossip_xml_validate (doc, CONTACT_GROUPS_DTD_FILENAME)) { + g_warning ("Failed to validate file:'%s'", filename); + xmlFreeDoc(doc); + xmlFreeParserCtxt (ctxt); + return; + } + + /* The root node, contacts. */ + contacts = xmlDocGetRootElement (doc); + + account = NULL; + node = contacts->children; + while (node) { + if (strcmp ((gchar *) node->name, "account") == 0) { + account = node; + break; + } + node = node->next; + } + + node = NULL; + if (account) { + node = account->children; + } + + while (node) { + if (strcmp ((gchar *) node->name, "group") == 0) { + gchar *name; + gchar *expanded_str; + gboolean expanded; + ContactGroup *contact_group; + + name = (gchar *) xmlGetProp (node, "name"); + expanded_str = (gchar *) xmlGetProp (node, "expanded"); + + if (expanded_str && strcmp (expanded_str, "yes") == 0) { + expanded = TRUE; + } else { + expanded = FALSE; + } + + contact_group = contact_group_new (name, expanded); + groups = g_list_append (groups, contact_group); + + xmlFree (name); + xmlFree (expanded_str); + } + + node = node->next; + } + + gossip_debug (DEBUG_DOMAIN, "Parsed %d contact groups", g_list_length (groups)); + + xmlFreeDoc(doc); + xmlFreeParserCtxt (ctxt); +} + +static ContactGroup * +contact_group_new (const gchar *name, + gboolean expanded) +{ + ContactGroup *group; + + group = g_new0 (ContactGroup, 1); + + group->name = g_strdup (name); + group->expanded = expanded; + + return group; +} + +static void +contact_group_free (ContactGroup *group) +{ + g_return_if_fail (group != NULL); + + g_free (group->name); + + g_free (group); +} + +static gboolean +contact_groups_file_save (void) +{ + xmlDocPtr doc; + xmlNodePtr root; + xmlNodePtr node; + GList *l; + gchar *dir; + gchar *file; + + dir = g_build_filename (g_get_home_dir (), ".gnome2", PACKAGE_NAME, NULL); + g_mkdir_with_parents (dir, S_IRUSR | S_IWUSR | S_IXUSR); + file = g_build_filename (dir, CONTACT_GROUPS_XML_FILENAME, NULL); + g_free (dir); + + doc = xmlNewDoc ("1.0"); + root = xmlNewNode (NULL, "contacts"); + xmlDocSetRootElement (doc, root); + + node = xmlNewChild (root, NULL, "account", NULL); + xmlNewProp (node, "name", "Default"); + + for (l = groups; l; l = l->next) { + ContactGroup *cg; + xmlNodePtr subnode; + + cg = l->data; + + subnode = xmlNewChild (node, NULL, "group", NULL); + xmlNewProp (subnode, "expanded", cg->expanded ? "yes" : "no"); + xmlNewProp (subnode, "name", cg->name); + } + + /* Make sure the XML is indented properly */ + xmlIndentTreeOutput = 1; + + gossip_debug (DEBUG_DOMAIN, "Saving file:'%s'", file); + xmlSaveFormatFileEnc (file, doc, "utf-8", 1); + xmlFreeDoc (doc); + + xmlCleanupParser (); + xmlMemoryDump (); + + g_free (file); + + return TRUE; +} + +gboolean +gossip_contact_group_get_expanded (const gchar *group) +{ + GList *l; + gboolean default_val = TRUE; + + g_return_val_if_fail (group != NULL, default_val); + + for (l = groups; l; l = l->next) { + ContactGroup *cg = l->data; + + if (!cg || !cg->name) { + continue; + } + + if (strcmp (cg->name, group) == 0) { + return cg->expanded; + } + } + + return default_val; +} + +void +gossip_contact_group_set_expanded (const gchar *group, + gboolean expanded) +{ + GList *l; + ContactGroup *cg; + gboolean changed = FALSE; + + g_return_if_fail (group != NULL); + + for (l = groups; l; l = l->next) { + ContactGroup *cg = l->data; + + if (!cg || !cg->name) { + continue; + } + + if (strcmp (cg->name, group) == 0) { + cg->expanded = expanded; + changed = TRUE; + break; + } + } + + /* if here... we don't have a ContactGroup for the group. */ + if (!changed) { + cg = contact_group_new (group, expanded); + groups = g_list_append (groups, cg); + } + + contact_groups_file_save (); +} diff --git a/libempathy-gtk/gossip-contact-groups.dtd b/libempathy-gtk/gossip-contact-groups.dtd new file mode 100644 index 00000000..689220f0 --- /dev/null +++ b/libempathy-gtk/gossip-contact-groups.dtd @@ -0,0 +1,17 @@ +<!-- + DTD for Gossips contact groups. + by Martyn Russell <mr@gnome.org> +--> + +<!-- Root element. --> +<!ELEMENT contacts (account)> + +<!ELEMENT account (group)+> +<!ATTLIST account + name CDATA #REQUIRED> + +<!-- Groups in the roster. --> +<!ELEMENT group EMPTY> +<!ATTLIST group + name CDATA #REQUIRED + expanded CDATA #REQUIRED> diff --git a/libempathy-gtk/gossip-contact-groups.h b/libempathy-gtk/gossip-contact-groups.h new file mode 100644 index 00000000..88bbdc0c --- /dev/null +++ b/libempathy-gtk/gossip-contact-groups.h @@ -0,0 +1,38 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2005 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Martyn Russell <martyn@imendio.com> + */ + +#ifndef __GOSSIP_CONTACT_GROUPS_H__ +#define __GOSSIP_CONTACT_GROUPS_H__ + +G_BEGIN_DECLS + +#include <glib.h> + +void gossip_contact_groups_get_all (void); + +gboolean gossip_contact_group_get_expanded (const gchar *group); +void gossip_contact_group_set_expanded (const gchar *group, + gboolean expanded); + +G_END_DECLS + +#endif /* __GOSSIP_CONTACT_GROUPS_H__ */ diff --git a/libempathy-gtk/gossip-contact-list.c b/libempathy-gtk/gossip-contact-list.c new file mode 100644 index 00000000..6e1573d8 --- /dev/null +++ b/libempathy-gtk/gossip-contact-list.c @@ -0,0 +1,2419 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2005-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Mikael Hallendal <micke@imendio.com> + * Martyn Russell <martyn@imendio.com> + */ + +#include "config.h" + +#include <string.h> + +#include <glib/gi18n.h> +#include <gtk/gtk.h> +#include <glade/glade.h> + +#include <libmissioncontrol/mc-account.h> + +#include <libempathy/empathy-contact-manager.h> +#include <libempathy/gossip-debug.h> +#include <libempathy/empathy-session.h> + +#include "gossip-contact-list.h" +#include "gossip-contact-groups.h" +#include "gossip-cell-renderer-expander.h" +#include "gossip-cell-renderer-text.h" +#include "gossip-stock.h" +#include "gossip-ui-utils.h" +//#include "gossip-chat-invite.h" +//#include "gossip-contact-info-dialog.h" +//#include "gossip-edit-contact-dialog.h" +//#include "gossip-ft-window.h" +//#include "gossip-log-window.h" + +#define DEBUG_DOMAIN "ContactListUI" + +/* Flashing delay for icons (milliseconds). */ +#define FLASH_TIMEOUT 500 + +/* Active users are those which have recently changed state + * (e.g. online, offline or from normal to a busy state). + */ + +/* Time user is shown as active */ +#define ACTIVE_USER_SHOW_TIME 7000 + +/* Time after connecting which we wait before active users are enabled */ +#define ACTIVE_USER_WAIT_TO_ENABLE_TIME 5000 + +#define GET_PRIV(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GOSSIP_TYPE_CONTACT_LIST, GossipContactListPriv)) + +struct _GossipContactListPriv { + EmpathyContactManager *manager; + + GHashTable *groups; + + GtkUIManager *ui; + GtkTreeRowReference *drag_row; + + gboolean show_offline; + gboolean show_avatars; + gboolean is_compact; + gboolean show_active; +}; + +typedef struct { + const gchar *name; + gboolean found; + GtkTreeIter iter; +} FindGroup; + +typedef struct { + GossipContact *contact; + gboolean found; + GList *iters; +} FindContact; + +typedef struct { + GossipContactList *list; + GtkTreePath *path; + guint timeout_id; +} DragMotionData; + +typedef struct { + GossipContactList *list; + GossipContact *contact; + gboolean remove; +} ShowActiveData; + +static void gossip_contact_list_class_init (GossipContactListClass *klass); +static void gossip_contact_list_init (GossipContactList *list); +static void contact_list_finalize (GObject *object); +static void contact_list_get_property (GObject *object, + guint param_id, + GValue *value, + GParamSpec *pspec); +static void contact_list_set_property (GObject *object, + guint param_id, + const GValue *value, + GParamSpec *pspec); +static void contact_list_contact_update (GossipContactList *list, + GossipContact *contact); +static void contact_list_contact_added_cb (EmpathyContactManager *manager, + GossipContact *contact, + GossipContactList *list); +static void contact_list_contact_updated_cb (GossipContact *contact, + GParamSpec *param, + GossipContactList *list); +static void contact_list_contact_groups_updated_cb (GossipContact *contact, + GParamSpec *param, + GossipContactList *list); +static void contact_list_contact_removed_cb (EmpathyContactManager *manager, + GossipContact *contact, + GossipContactList *list); +static void contact_list_contact_set_active (GossipContactList *list, + GossipContact *contact, + gboolean active, + gboolean set_changed); +static ShowActiveData * + contact_list_contact_active_new (GossipContactList *list, + GossipContact *contact, + gboolean remove); +static void contact_list_contact_active_free (ShowActiveData *data); +static gboolean contact_list_contact_active_cb (ShowActiveData *data); +static gchar * contact_list_get_parent_group (GtkTreeModel *model, + GtkTreePath *path, + gboolean *path_is_group); +static void contact_list_get_group (GossipContactList *list, + const gchar *name, + GtkTreeIter *iter_to_set, + gboolean *created); +static gboolean contact_list_get_group_foreach (GtkTreeModel *model, + GtkTreePath *path, + GtkTreeIter *iter, + FindGroup *fg); +static void contact_list_add_contact (GossipContactList *list, + GossipContact *contact); +static void contact_list_remove_contact (GossipContactList *list, + GossipContact *contact); +static void contact_list_create_model (GossipContactList *list); +static gboolean contact_list_search_equal_func (GtkTreeModel *model, + gint column, + const gchar *key, + GtkTreeIter *iter, + gpointer search_data); +static void contact_list_setup_view (GossipContactList *list); +static void contact_list_drag_data_received (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + GtkSelectionData *selection, + guint info, + guint time, + gpointer user_data); +static gboolean contact_list_drag_motion (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + guint time, + gpointer data); +static gboolean contact_list_drag_motion_cb (DragMotionData *data); +static void contact_list_drag_begin (GtkWidget *widget, + GdkDragContext *context, + gpointer user_data); +static void contact_list_drag_data_get (GtkWidget *widget, + GdkDragContext *contact, + GtkSelectionData *selection, + guint info, + guint time, + gpointer user_data); +static void contact_list_drag_end (GtkWidget *widget, + GdkDragContext *context, + gpointer user_data); +static void contact_list_cell_set_background (GossipContactList *list, + GtkCellRenderer *cell, + gboolean is_group, + gboolean is_active); +static void contact_list_pixbuf_cell_data_func (GtkTreeViewColumn *tree_column, + GtkCellRenderer *cell, + GtkTreeModel *model, + GtkTreeIter *iter, + GossipContactList *list); +static void contact_list_avatar_cell_data_func (GtkTreeViewColumn *tree_column, + GtkCellRenderer *cell, + GtkTreeModel *model, + GtkTreeIter *iter, + GossipContactList *list); +static void contact_list_text_cell_data_func (GtkTreeViewColumn *tree_column, + GtkCellRenderer *cell, + GtkTreeModel *model, + GtkTreeIter *iter, + GossipContactList *list); +static void contact_list_expander_cell_data_func (GtkTreeViewColumn *column, + GtkCellRenderer *cell, + GtkTreeModel *model, + GtkTreeIter *iter, + GossipContactList *list); +static GtkWidget *contact_list_get_contact_menu (GossipContactList *list, + gboolean can_send_file, + gboolean can_show_log); +static gboolean contact_list_button_press_event_cb (GossipContactList *list, + GdkEventButton *event, + gpointer user_data); +static void contact_list_row_activated_cb (GossipContactList *list, + GtkTreePath *path, + GtkTreeViewColumn *col, + gpointer user_data); +static void contact_list_row_expand_or_collapse_cb (GossipContactList *list, + GtkTreeIter *iter, + GtkTreePath *path, + gpointer user_data); +static gint contact_list_sort_func (GtkTreeModel *model, + GtkTreeIter *iter_a, + GtkTreeIter *iter_b, + gpointer user_data); +static GList * contact_list_find_contact (GossipContactList *list, + GossipContact *contact); +static gboolean contact_list_find_contact_foreach (GtkTreeModel *model, + GtkTreePath *path, + GtkTreeIter *iter, + FindContact *fc); +static void contact_list_action_cb (GtkAction *action, + GossipContactList *list); +static gboolean contact_list_update_list_mode_foreach (GtkTreeModel *model, + GtkTreePath *path, + GtkTreeIter *iter, + GossipContactList *list); +enum { + CONTACT_CHAT, + CONTACT_INFORMATION, + CONTACT_EDIT, + CONTACT_REMOVE, + CONTACT_INVITE, + CONTACT_SEND_FILE, + CONTACT_LOG, + GROUP_RENAME, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +enum { + COL_PIXBUF_STATUS, + COL_PIXBUF_AVATAR, + COL_PIXBUF_AVATAR_VISIBLE, + COL_NAME, + COL_STATUS, + COL_STATUS_VISIBLE, + COL_CONTACT, + COL_IS_GROUP, + COL_IS_ACTIVE, + COL_IS_ONLINE, + COL_COUNT +}; + +enum { + PROP_0, + PROP_SHOW_OFFLINE, + PROP_SHOW_AVATARS, + PROP_IS_COMPACT, +}; + +static const GtkActionEntry entries[] = { + { "ContactMenu", NULL, + N_("_Contact"), NULL, NULL, + NULL + }, + { "GroupMenu", NULL, + N_("_Group"),NULL, NULL, + NULL + }, + { "Chat", GOSSIP_STOCK_MESSAGE, + N_("_Chat"), NULL, N_("Chat with contact"), + G_CALLBACK (contact_list_action_cb) + }, + { "Information", GOSSIP_STOCK_CONTACT_INFORMATION, + N_("Infor_mation"), "<control>I", N_("View contact information"), + G_CALLBACK (contact_list_action_cb) + }, + { "Rename", NULL, + N_("Re_name"), NULL, N_("Rename"), + G_CALLBACK (contact_list_action_cb) + }, + { "Edit", GTK_STOCK_EDIT, + N_("_Edit"), NULL, N_("Edit the groups and name for this contact"), + G_CALLBACK (contact_list_action_cb) + }, + { "Remove", GTK_STOCK_REMOVE, + N_("_Remove"), NULL, N_("Remove contact"), + G_CALLBACK (contact_list_action_cb) + }, + { "Invite", GOSSIP_STOCK_GROUP_MESSAGE, + N_("_Invite to Chat Room"), NULL, N_("Invite to a currently open chat room"), + G_CALLBACK (contact_list_action_cb) + }, + { "SendFile", NULL, + N_("_Send File..."), NULL, N_("Send a file"), + G_CALLBACK (contact_list_action_cb) + }, + { "Log", GTK_STOCK_JUSTIFY_LEFT, + N_("_View Previous Conversations"), NULL, N_("View previous conversations with this contact"), + G_CALLBACK (contact_list_action_cb) + }, +}; + +static guint n_entries = G_N_ELEMENTS (entries); + +static const gchar *ui_info = + "<ui>" + " <popup name='Contact'>" + " <menuitem action='Chat'/>" + " <menuitem action='Log'/>" + " <menuitem action='SendFile'/>" + " <separator/>" + " <menuitem action='Invite'/>" + " <separator/>" + " <menuitem action='Edit'/>" + " <menuitem action='Remove'/>" + " <separator/>" + " <menuitem action='Information'/>" + " </popup>" + " <popup name='Group'>" + " <menuitem action='Rename'/>" + " </popup>" + "</ui>"; + +enum DndDragType { + DND_DRAG_TYPE_CONTACT_ID, + DND_DRAG_TYPE_URL, + DND_DRAG_TYPE_STRING, +}; + +static const GtkTargetEntry drag_types_dest[] = { + { "text/contact-id", 0, DND_DRAG_TYPE_CONTACT_ID }, + { "text/uri-list", 0, DND_DRAG_TYPE_URL }, + { "text/plain", 0, DND_DRAG_TYPE_STRING }, + { "STRING", 0, DND_DRAG_TYPE_STRING }, +}; + +static const GtkTargetEntry drag_types_source[] = { + { "text/contact-id", 0, DND_DRAG_TYPE_CONTACT_ID }, +}; + +static GdkAtom drag_atoms_dest[G_N_ELEMENTS (drag_types_dest)]; +static GdkAtom drag_atoms_source[G_N_ELEMENTS (drag_types_source)]; + +G_DEFINE_TYPE (GossipContactList, gossip_contact_list, GTK_TYPE_TREE_VIEW); + +static void +gossip_contact_list_class_init (GossipContactListClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = contact_list_finalize; + object_class->get_property = contact_list_get_property; + object_class->set_property = contact_list_set_property; + + signals[CONTACT_CHAT] = + g_signal_new ("contact-chat", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, + 1, GOSSIP_TYPE_CONTACT); + signals[CONTACT_INFORMATION] = + g_signal_new ("contact-information", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, + 1, GOSSIP_TYPE_CONTACT); + signals[CONTACT_EDIT] = + g_signal_new ("contact-edit", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, + 1, GOSSIP_TYPE_CONTACT); + signals[CONTACT_REMOVE] = + g_signal_new ("contact-remove", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, + 1, GOSSIP_TYPE_CONTACT); + signals[CONTACT_INVITE] = + g_signal_new ("contact-invite", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, + 1, GOSSIP_TYPE_CONTACT); + signals[CONTACT_SEND_FILE] = + g_signal_new ("contact-send-file", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, + 1, GOSSIP_TYPE_CONTACT); + signals[CONTACT_LOG] = + g_signal_new ("contact-log", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, + 1, GOSSIP_TYPE_CONTACT); + signals[GROUP_RENAME] = + g_signal_new ("group-rename", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__STRING, + G_TYPE_NONE, + 1, G_TYPE_STRING); + + + g_object_class_install_property (object_class, + PROP_SHOW_OFFLINE, + g_param_spec_boolean ("show-offline", + "Show Offline", + "Whether contact list should display " + "offline contacts", + FALSE, + G_PARAM_READWRITE)); + g_object_class_install_property (object_class, + PROP_SHOW_AVATARS, + g_param_spec_boolean ("show-avatars", + "Show Avatars", + "Whether contact list should display " + "avatars for contacts", + TRUE, + G_PARAM_READWRITE)); + g_object_class_install_property (object_class, + PROP_IS_COMPACT, + g_param_spec_boolean ("is-compact", + "Is Compact", + "Whether the contact list is in compact mode or not", + FALSE, + G_PARAM_READWRITE)); + + g_type_class_add_private (object_class, sizeof (GossipContactListPriv)); +} + +static void +gossip_contact_list_init (GossipContactList *list) +{ + GossipContactListPriv *priv; + GtkActionGroup *action_group; + GList *contacts, *l; + GError *error = NULL; + + priv = GET_PRIV (list); + + priv->manager = empathy_session_get_contact_manager (); + g_object_ref (priv->manager); + priv->is_compact = FALSE; + priv->show_active = TRUE; + priv->show_avatars = TRUE; + + contact_list_create_model (list); + contact_list_setup_view (list); + empathy_contact_manager_setup (priv->manager); + + /* Get saved group states. */ + gossip_contact_groups_get_all (); + + /* Set up UI Manager */ + priv->ui = gtk_ui_manager_new (); + + action_group = gtk_action_group_new ("Actions"); + gtk_action_group_set_translation_domain (action_group, GETTEXT_PACKAGE); + gtk_action_group_add_actions (action_group, entries, n_entries, list); + gtk_ui_manager_insert_action_group (priv->ui, action_group, 0); + + if (!gtk_ui_manager_add_ui_from_string (priv->ui, ui_info, -1, &error)) { + g_warning ("Could not build contact menus from string:'%s'", error->message); + g_error_free (error); + } + + g_object_unref (action_group); + + /* Signal connection. */ + g_signal_connect (priv->manager, + "contact-added", + G_CALLBACK (contact_list_contact_added_cb), + list); + g_signal_connect (priv->manager, + "contact-removed", + G_CALLBACK (contact_list_contact_removed_cb), + list); + + /* Connect to tree view signals rather than override. */ + g_signal_connect (list, + "button-press-event", + G_CALLBACK (contact_list_button_press_event_cb), + NULL); + g_signal_connect (list, + "row-activated", + G_CALLBACK (contact_list_row_activated_cb), + NULL); + g_signal_connect (list, + "row-expanded", + G_CALLBACK (contact_list_row_expand_or_collapse_cb), + GINT_TO_POINTER (TRUE)); + g_signal_connect (list, + "row-collapsed", + G_CALLBACK (contact_list_row_expand_or_collapse_cb), + GINT_TO_POINTER (FALSE)); + + /* Add contacts already created */ + contacts = empathy_contact_manager_get_contacts (priv->manager); + for (l = contacts; l; l = l->next) { + GossipContact *contact; + + contact = l->data; + + contact_list_contact_added_cb (priv->manager, contact, list); + + g_object_unref (contact); + } +} + +static void +contact_list_finalize (GObject *object) +{ + GossipContactListPriv *priv; + + priv = GET_PRIV (object); + + /* FIXME: disconnect all signals on the manager and contacts */ + + g_object_unref (priv->manager); + g_object_unref (priv->ui); + + G_OBJECT_CLASS (gossip_contact_list_parent_class)->finalize (object); +} + +static void +contact_list_get_property (GObject *object, + guint param_id, + GValue *value, + GParamSpec *pspec) +{ + GossipContactListPriv *priv; + + priv = GET_PRIV (object); + + switch (param_id) { + case PROP_SHOW_OFFLINE: + g_value_set_boolean (value, priv->show_offline); + break; + case PROP_SHOW_AVATARS: + g_value_set_boolean (value, priv->show_avatars); + break; + case PROP_IS_COMPACT: + g_value_set_boolean (value, priv->is_compact); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec); + break; + }; +} + +static void +contact_list_set_property (GObject *object, + guint param_id, + const GValue *value, + GParamSpec *pspec) +{ + GossipContactListPriv *priv; + + priv = GET_PRIV (object); + + switch (param_id) { + case PROP_SHOW_OFFLINE: + gossip_contact_list_set_show_offline (GOSSIP_CONTACT_LIST (object), + g_value_get_boolean (value)); + break; + case PROP_SHOW_AVATARS: + gossip_contact_list_set_show_avatars (GOSSIP_CONTACT_LIST (object), + g_value_get_boolean (value)); + break; + case PROP_IS_COMPACT: + gossip_contact_list_set_is_compact (GOSSIP_CONTACT_LIST (object), + g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec); + break; + }; +} + +static void +contact_list_contact_update (GossipContactList *list, + GossipContact *contact) +{ + GossipContactListPriv *priv; + ShowActiveData *data; + GtkTreeModel *model; + GList *iters, *l; + gboolean in_list; + gboolean should_be_in_list; + gboolean was_online = TRUE; + gboolean now_online = FALSE; + gboolean set_model = FALSE; + gboolean do_remove = FALSE; + gboolean do_set_active = FALSE; + gboolean do_set_refresh = FALSE; + GdkPixbuf *pixbuf_presence; + GdkPixbuf *pixbuf_avatar; + + priv = GET_PRIV (list); + + model = gtk_tree_view_get_model (GTK_TREE_VIEW (list)); + + iters = contact_list_find_contact (list, contact); + if (!iters) { + in_list = FALSE; + } else { + in_list = TRUE; + } + + /* Get online state now. */ + now_online = gossip_contact_is_online (contact); + + if (priv->show_offline || now_online) { + should_be_in_list = TRUE; + } else { + should_be_in_list = FALSE; + } + + if (!in_list && !should_be_in_list) { + /* Nothing to do. */ + gossip_debug (DEBUG_DOMAIN, + "Contact:'%s' in list:NO, should be:NO", + gossip_contact_get_name (contact)); + + g_list_foreach (iters, (GFunc) gtk_tree_iter_free, NULL); + g_list_free (iters); + return; + } + else if (in_list && !should_be_in_list) { + gossip_debug (DEBUG_DOMAIN, + "Contact:'%s' in list:YES, should be:NO", + gossip_contact_get_name (contact)); + + if (priv->show_active) { + do_remove = TRUE; + do_set_active = TRUE; + do_set_refresh = TRUE; + + set_model = TRUE; + gossip_debug (DEBUG_DOMAIN, "Remove item (after timeout)"); + } else { + gossip_debug (DEBUG_DOMAIN, "Remove item (now)!"); + contact_list_remove_contact (list, contact); + } + } + else if (!in_list && should_be_in_list) { + gossip_debug (DEBUG_DOMAIN, + "Contact:'%s' in list:NO, should be:YES", + gossip_contact_get_name (contact)); + + contact_list_add_contact (list, contact); + + if (priv->show_active) { + do_set_active = TRUE; + + gossip_debug (DEBUG_DOMAIN, "Set active (contact added)"); + } + } else { + gossip_debug (DEBUG_DOMAIN, + "Contact:'%s' in list:YES, should be:YES", + gossip_contact_get_name (contact)); + + /* Get online state before. */ + if (iters && g_list_length (iters) > 0) { + GtkTreeIter *iter; + + iter = g_list_nth_data (iters, 0); + gtk_tree_model_get (model, iter, COL_IS_ONLINE, &was_online, -1); + } + + /* Is this really an update or an online/offline. */ + if (priv->show_active) { + if (was_online != now_online) { + gchar *str; + + do_set_active = TRUE; + do_set_refresh = TRUE; + + if (was_online) { + str = "online -> offline"; + } else { + str = "offline -> online"; + } + + gossip_debug (DEBUG_DOMAIN, "Set active (contact updated %s)", str); + } else { + /* Was TRUE for presence updates. */ + /* do_set_active = FALSE; */ + do_set_refresh = TRUE; + + gossip_debug (DEBUG_DOMAIN, "Set active (contact updated)"); + } + } + + set_model = TRUE; + } + + pixbuf_presence = gossip_pixbuf_for_contact (contact); + pixbuf_avatar = gossip_pixbuf_avatar_from_contact_scaled (contact, 32, 32); + for (l = iters; l && set_model; l = l->next) { + gtk_tree_store_set (GTK_TREE_STORE (model), l->data, + COL_PIXBUF_STATUS, pixbuf_presence, + COL_STATUS, gossip_contact_get_status (contact), + COL_IS_ONLINE, now_online, + COL_NAME, gossip_contact_get_name (contact), + COL_PIXBUF_AVATAR, pixbuf_avatar, + -1); + } + + if (pixbuf_presence) { + g_object_unref (pixbuf_presence); + } + if (pixbuf_avatar) { + g_object_unref (pixbuf_avatar); + } + + if (priv->show_active && do_set_active) { + contact_list_contact_set_active (list, contact, do_set_active, do_set_refresh); + + if (do_set_active) { + data = contact_list_contact_active_new (list, contact, do_remove); + g_timeout_add (ACTIVE_USER_SHOW_TIME, + (GSourceFunc) contact_list_contact_active_cb, + data); + } + } + + /* FIXME: when someone goes online then offline quickly, the + * first timeout sets the user to be inactive and the second + * timeout removes the user from the contact list, really we + * should remove the first timeout. + */ + g_list_foreach (iters, (GFunc) gtk_tree_iter_free, NULL); + g_list_free (iters); +} + +static void +contact_list_contact_added_cb (EmpathyContactManager *manager, + GossipContact *contact, + GossipContactList *list) +{ + GossipContactListPriv *priv; + + priv = GET_PRIV (list); + + gossip_debug (DEBUG_DOMAIN, "Contact:'%s' added", + gossip_contact_get_name (contact)); + + /* Connect notifications for contact updates */ + g_signal_connect (contact, "notify::groups", + G_CALLBACK (contact_list_contact_groups_updated_cb), + list); + g_signal_connect (contact, "notify::presences", + G_CALLBACK (contact_list_contact_updated_cb), + list); + g_signal_connect (contact, "notify::name", + G_CALLBACK (contact_list_contact_updated_cb), + list); + g_signal_connect (contact, "notify::avatar", + G_CALLBACK (contact_list_contact_updated_cb), + list); + g_signal_connect (contact, "notify::type", + G_CALLBACK (contact_list_contact_updated_cb), + list); + + contact_list_add_contact (list, contact); +} + +static void +contact_list_contact_groups_updated_cb (GossipContact *contact, + GParamSpec *param, + GossipContactList *list) +{ + GossipContactListPriv *priv; + + priv = GET_PRIV (list); + + if (priv->show_offline || gossip_contact_is_online (contact)) { + } + + + gossip_debug (DEBUG_DOMAIN, "Contact:'%s' groups updated", + gossip_contact_get_name (contact)); + + /* We do this to make sure the groups are correct, if not, we + * would have to check the groups already set up for each + * contact and then see what has been updated. + */ + contact_list_remove_contact (list, contact); + contact_list_add_contact (list, contact); +} + +static void +contact_list_contact_updated_cb (GossipContact *contact, + GParamSpec *param, + GossipContactList *list) +{ + contact_list_contact_update (list, contact); +} + +static void +contact_list_contact_removed_cb (EmpathyContactManager *manager, + GossipContact *contact, + GossipContactList *list) +{ + gossip_debug (DEBUG_DOMAIN, "Contact:'%s' removed", + gossip_contact_get_name (contact)); + + /* Disconnect signals */ + g_signal_handlers_disconnect_by_func (contact, + G_CALLBACK (contact_list_contact_groups_updated_cb), + list); + g_signal_handlers_disconnect_by_func (contact, + G_CALLBACK (contact_list_contact_updated_cb), + list); + + contact_list_remove_contact (list, contact); +} + +static void +contact_list_contact_set_active (GossipContactList *list, + GossipContact *contact, + gboolean active, + gboolean set_changed) +{ + GtkTreeModel *model; + GList *iters, *l; + + model = gtk_tree_view_get_model (GTK_TREE_VIEW (list)); + + iters = contact_list_find_contact (list, contact); + for (l = iters; l; l = l->next) { + GtkTreePath *path; + GtkTreeIter *iter; + + iter = l->data; + + gtk_tree_store_set (GTK_TREE_STORE (model), iter, + COL_IS_ACTIVE, active, + -1); + gossip_debug (DEBUG_DOMAIN, "Set item %s", active ? "active" : "inactive"); + + if (set_changed) { + path = gtk_tree_model_get_path (model, iter); + gtk_tree_model_row_changed (model, path, iter); + gtk_tree_path_free (path); + } + } + + g_list_foreach (iters, (GFunc)gtk_tree_iter_free, NULL); + g_list_free (iters); +} + +static ShowActiveData * +contact_list_contact_active_new (GossipContactList *list, + GossipContact *contact, + gboolean remove) +{ + ShowActiveData *data; + + g_return_val_if_fail (list != NULL, NULL); + g_return_val_if_fail (contact != NULL, NULL); + + gossip_debug (DEBUG_DOMAIN, + "Contact:'%s' now active, and %s be removed", + gossip_contact_get_name (contact), + remove ? "WILL" : "WILL NOT"); + + data = g_slice_new0 (ShowActiveData); + + data->list = g_object_ref (list); + data->contact = g_object_ref (contact); + + data->remove = remove; + + return data; +} + +static void +contact_list_contact_active_free (ShowActiveData *data) +{ + g_return_if_fail (data != NULL); + + g_object_unref (data->contact); + g_object_unref (data->list); + + g_slice_free (ShowActiveData, data); +} + +static gboolean +contact_list_contact_active_cb (ShowActiveData *data) +{ + GossipContactListPriv *priv; + + g_return_val_if_fail (data != NULL, FALSE); + + priv = GET_PRIV (data->list); + + if (data->remove && + !priv->show_offline && + !gossip_contact_is_online (data->contact)) { + gossip_debug (DEBUG_DOMAIN, + "Contact:'%s' active timeout, removing item", + gossip_contact_get_name (data->contact)); + contact_list_remove_contact (data->list, + data->contact); + } + + gossip_debug (DEBUG_DOMAIN, + "Contact:'%s' no longer active", + gossip_contact_get_name (data->contact)); + contact_list_contact_set_active (data->list, + data->contact, + FALSE, + TRUE); + + contact_list_contact_active_free (data); + + return FALSE; +} + +static gchar * +contact_list_get_parent_group (GtkTreeModel *model, + GtkTreePath *path, + gboolean *path_is_group) +{ + GtkTreeIter parent_iter, iter; + gchar *name; + gboolean is_group; + + g_return_val_if_fail (model != NULL, NULL); + g_return_val_if_fail (path != NULL, NULL); + g_return_val_if_fail (path_is_group != NULL, NULL); + + if (!gtk_tree_model_get_iter (model, &iter, path)) { + return NULL; + } + + gtk_tree_model_get (model, &iter, + COL_IS_GROUP, &is_group, + -1); + + if (!is_group) { + if (!gtk_tree_model_iter_parent (model, &parent_iter, &iter)) { + return NULL; + } + + iter = parent_iter; + + gtk_tree_model_get (model, &iter, + COL_IS_GROUP, &is_group, + -1); + + if (!is_group) { + return NULL; + } + + *path_is_group = TRUE; + } + + gtk_tree_model_get (model, &iter, + COL_NAME, &name, + -1); + + return name; +} + +static void +contact_list_get_group (GossipContactList *list, + const gchar *name, + GtkTreeIter *iter_to_set, + gboolean *created) +{ + GtkTreeModel *model; + FindGroup fg; + + memset (&fg, 0, sizeof (fg)); + + fg.name = name; + + model = gtk_tree_view_get_model (GTK_TREE_VIEW (list)); + gtk_tree_model_foreach (model, + (GtkTreeModelForeachFunc) contact_list_get_group_foreach, + &fg); + + if (!fg.found) { + if (created) { + *created = TRUE; + } + + gtk_tree_store_append (GTK_TREE_STORE (model), iter_to_set, NULL); + gtk_tree_store_set (GTK_TREE_STORE (model), iter_to_set, + COL_PIXBUF_STATUS, NULL, + COL_NAME, name, + COL_IS_GROUP, TRUE, + COL_IS_ACTIVE, FALSE, + -1); + } else { + if (created) { + *created = FALSE; + } + + *iter_to_set = fg.iter; + } +} + +static gboolean +contact_list_get_group_foreach (GtkTreeModel *model, + GtkTreePath *path, + GtkTreeIter *iter, + FindGroup *fg) +{ + gchar *str; + gboolean is_group; + + /* Groups are only at the top level. */ + if (gtk_tree_path_get_depth (path) != 1) { + return FALSE; + } + + gtk_tree_model_get (model, iter, + COL_NAME, &str, + COL_IS_GROUP, &is_group, + -1); + if (is_group && strcmp (str, fg->name) == 0) { + fg->found = TRUE; + fg->iter = *iter; + } + + g_free (str); + + return fg->found; +} + +static void +contact_list_add_contact (GossipContactList *list, + GossipContact *contact) +{ + GossipContactListPriv *priv; + GtkTreeIter iter, iter_group; + GtkTreeModel *model; + GList *l, *groups; + + priv = GET_PRIV (list); + + if (!priv->show_offline && !gossip_contact_is_online (contact)) { + return; + } + + model = gtk_tree_view_get_model (GTK_TREE_VIEW (list)); + + /* If no groups just add it at the top level. */ + groups = gossip_contact_get_groups (contact); + if (!groups) { + GdkPixbuf *pixbuf_status; + GdkPixbuf *pixbuf_avatar; + gboolean show_avatar = FALSE; + + pixbuf_status = gossip_pixbuf_for_contact (contact); + pixbuf_avatar = gossip_pixbuf_avatar_from_contact_scaled ( + contact, 32, 32); + + if (priv->show_avatars && !priv->is_compact) { + show_avatar = TRUE; + } + + gtk_tree_store_append (GTK_TREE_STORE (model), &iter, NULL); + gtk_tree_store_set (GTK_TREE_STORE (model), &iter, + COL_PIXBUF_STATUS, pixbuf_status, + COL_PIXBUF_AVATAR, pixbuf_avatar, + COL_PIXBUF_AVATAR_VISIBLE, show_avatar, + COL_NAME, gossip_contact_get_name (contact), + COL_STATUS, gossip_contact_get_status (contact), + COL_STATUS_VISIBLE, !priv->is_compact, + COL_CONTACT, contact, + COL_IS_GROUP, FALSE, + COL_IS_ACTIVE, FALSE, + COL_IS_ONLINE, gossip_contact_is_online (contact), + -1); + + if (pixbuf_avatar) { + g_object_unref (pixbuf_avatar); + } + if (pixbuf_status) { + g_object_unref (pixbuf_status); + } + } + + /* Else add to each group. */ + for (l = groups; l; l = l->next) { + GtkTreePath *path; + GdkPixbuf *pixbuf_status; + GdkPixbuf *pixbuf_avatar; + const gchar *name; + gboolean created; + gboolean show_avatar = FALSE; + + name = l->data; + if (!name) { + continue; + } + + pixbuf_status = gossip_pixbuf_for_contact (contact); + pixbuf_avatar = gossip_pixbuf_avatar_from_contact_scaled ( + contact, 32, 32); + + contact_list_get_group (list, name, &iter_group, &created); + + if (priv->show_avatars && !priv->is_compact) { + show_avatar = TRUE; + } + + gtk_tree_store_append (GTK_TREE_STORE (model), &iter, &iter_group); + gtk_tree_store_set (GTK_TREE_STORE (model), &iter, + COL_PIXBUF_STATUS, pixbuf_status, + COL_PIXBUF_AVATAR, pixbuf_avatar, + COL_PIXBUF_AVATAR_VISIBLE, show_avatar, + COL_NAME, gossip_contact_get_name (contact), + COL_STATUS, gossip_contact_get_status (contact), + COL_STATUS_VISIBLE, !priv->is_compact, + COL_CONTACT, contact, + COL_IS_GROUP, FALSE, + COL_IS_ACTIVE, FALSE, + COL_IS_ONLINE, gossip_contact_is_online (contact), + -1); + + if (pixbuf_avatar) { + g_object_unref (pixbuf_avatar); + } + if (pixbuf_status) { + g_object_unref (pixbuf_status); + } + + if (!created) { + continue; + } + + path = gtk_tree_model_get_path (model, &iter_group); + if (!path) { + continue; + } + + if (gossip_contact_group_get_expanded (name)) { + g_signal_handlers_block_by_func (list, + contact_list_row_expand_or_collapse_cb, + GINT_TO_POINTER (TRUE)); + gtk_tree_view_expand_row (GTK_TREE_VIEW (list), path, TRUE); + g_signal_handlers_unblock_by_func (list, + contact_list_row_expand_or_collapse_cb, + GINT_TO_POINTER (TRUE)); + } else { + g_signal_handlers_block_by_func (list, + contact_list_row_expand_or_collapse_cb, + GINT_TO_POINTER (FALSE)); + gtk_tree_view_collapse_row (GTK_TREE_VIEW (list), path); + g_signal_handlers_unblock_by_func (list, + contact_list_row_expand_or_collapse_cb, + GINT_TO_POINTER (FALSE)); + } + + gtk_tree_path_free (path); + } +} + +static void +contact_list_remove_contact (GossipContactList *list, + GossipContact *contact) +{ + GossipContactListPriv *priv; + GtkTreeModel *model; + GList *iters, *l; + + priv = GET_PRIV (list); + + iters = contact_list_find_contact (list, contact); + if (!iters) { + return; + } + + /* Clean up model */ + model = gtk_tree_view_get_model (GTK_TREE_VIEW (list)); + + for (l = iters; l; l = l->next) { + GtkTreeIter parent; + + if (gtk_tree_model_iter_parent (model, &parent, l->data) && + gtk_tree_model_iter_n_children (model, &parent) <= 1) { + gtk_tree_store_remove (GTK_TREE_STORE (model), &parent); + } else { + gtk_tree_store_remove (GTK_TREE_STORE (model), l->data); + } + } + + g_list_foreach (iters, (GFunc)gtk_tree_iter_free, NULL); + g_list_free (iters); +} + +static void +contact_list_create_model (GossipContactList *list) +{ + GtkTreeModel *model; + + model = GTK_TREE_MODEL ( + gtk_tree_store_new (COL_COUNT, + GDK_TYPE_PIXBUF, /* Status pixbuf */ + GDK_TYPE_PIXBUF, /* Avatar pixbuf */ + G_TYPE_BOOLEAN, /* Avatar pixbuf visible */ + G_TYPE_STRING, /* Name */ + G_TYPE_STRING, /* Status string */ + G_TYPE_BOOLEAN, /* Show status */ + GOSSIP_TYPE_CONTACT, /* Contact type */ + G_TYPE_BOOLEAN, /* Is group */ + G_TYPE_BOOLEAN, /* Is active */ + G_TYPE_BOOLEAN)); /* Is online */ + + gtk_tree_sortable_set_sort_func (GTK_TREE_SORTABLE (model), + COL_NAME, + contact_list_sort_func, + list, NULL); + + gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (model), + COL_NAME, + GTK_SORT_ASCENDING); + + gtk_tree_view_set_model (GTK_TREE_VIEW (list), model); + + g_object_unref (model); +} + +static gboolean +contact_list_search_equal_func (GtkTreeModel *model, + gint column, + const gchar *key, + GtkTreeIter *iter, + gpointer search_data) +{ + gchar *name, *name_folded; + gchar *key_folded; + gboolean ret; + + gtk_tree_model_get (model, iter, + COL_NAME, &name, + -1); + + name_folded = g_utf8_casefold (name, -1); + key_folded = g_utf8_casefold (key, -1); + + if (strstr (name_folded, key_folded)) { + ret = FALSE; + } else { + ret = TRUE; + } + + g_free (name); + g_free (name_folded); + g_free (key_folded); + + return ret; +} + +static void +contact_list_setup_view (GossipContactList *list) +{ + GtkCellRenderer *cell; + GtkTreeViewColumn *col; + gint i; + + gtk_tree_view_set_search_equal_func (GTK_TREE_VIEW (list), + contact_list_search_equal_func, + list, + NULL); + + g_object_set (list, + "headers-visible", FALSE, + "reorderable", TRUE, + "show-expanders", FALSE, + NULL); + + col = gtk_tree_view_column_new (); + + /* State */ + cell = gtk_cell_renderer_pixbuf_new (); + gtk_tree_view_column_pack_start (col, cell, FALSE); + gtk_tree_view_column_set_cell_data_func ( + col, cell, + (GtkTreeCellDataFunc) contact_list_pixbuf_cell_data_func, + list, NULL); + + g_object_set (cell, + "xpad", 5, + "ypad", 1, + "visible", FALSE, + NULL); + + /* Name */ + cell = gossip_cell_renderer_text_new (); + gtk_tree_view_column_pack_start (col, cell, TRUE); + gtk_tree_view_column_set_cell_data_func ( + col, cell, + (GtkTreeCellDataFunc) contact_list_text_cell_data_func, + list, NULL); + + gtk_tree_view_column_add_attribute (col, cell, + "name", COL_NAME); + gtk_tree_view_column_add_attribute (col, cell, + "status", COL_STATUS); + gtk_tree_view_column_add_attribute (col, cell, + "is_group", COL_IS_GROUP); + + /* Avatar */ + cell = gtk_cell_renderer_pixbuf_new (); + gtk_tree_view_column_pack_start (col, cell, FALSE); + gtk_tree_view_column_set_cell_data_func ( + col, cell, + (GtkTreeCellDataFunc) contact_list_avatar_cell_data_func, + list, NULL); + + g_object_set (cell, + "xpad", 0, + "ypad", 0, + "visible", FALSE, + "width", 32, + "height", 32, + NULL); + + /* Expander */ + cell = gossip_cell_renderer_expander_new (); + gtk_tree_view_column_pack_end (col, cell, FALSE); + gtk_tree_view_column_set_cell_data_func ( + col, cell, + (GtkTreeCellDataFunc) contact_list_expander_cell_data_func, + list, NULL); + + /* Actually add the column now we have added all cell renderers */ + gtk_tree_view_append_column (GTK_TREE_VIEW (list), col); + + /* Drag & Drop. */ + for (i = 0; i < G_N_ELEMENTS (drag_types_dest); ++i) { + drag_atoms_dest[i] = gdk_atom_intern (drag_types_dest[i].target, + FALSE); + } + + for (i = 0; i < G_N_ELEMENTS (drag_types_source); ++i) { + drag_atoms_source[i] = gdk_atom_intern (drag_types_source[i].target, + FALSE); + } + + /* Note: We support the COPY action too, but need to make the + * MOVE action the default. + */ + gtk_drag_source_set (GTK_WIDGET (list), + GDK_BUTTON1_MASK, + drag_types_source, + G_N_ELEMENTS (drag_types_source), + GDK_ACTION_MOVE); + + gtk_drag_dest_set (GTK_WIDGET (list), + GTK_DEST_DEFAULT_ALL, + drag_types_dest, + G_N_ELEMENTS (drag_types_dest), + GDK_ACTION_MOVE | GDK_ACTION_LINK); + + g_signal_connect (GTK_WIDGET (list), + "drag-data-received", + G_CALLBACK (contact_list_drag_data_received), + NULL); + + /* FIXME: noticed but when you drag the row over the treeview + * fast, it seems to stop redrawing itself, if we don't + * connect this signal, all is fine. + */ + g_signal_connect (GTK_WIDGET (list), + "drag-motion", + G_CALLBACK (contact_list_drag_motion), + NULL); + + g_signal_connect (GTK_WIDGET (list), + "drag-begin", + G_CALLBACK (contact_list_drag_begin), + NULL); + g_signal_connect (GTK_WIDGET (list), + "drag-data-get", + G_CALLBACK (contact_list_drag_data_get), + NULL); + g_signal_connect (GTK_WIDGET (list), + "drag-end", + G_CALLBACK (contact_list_drag_end), + NULL); +} + +static void +contact_list_drag_data_received (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + GtkSelectionData *selection, + guint info, + guint time, + gpointer user_data) +{ + GossipContactListPriv *priv; + GtkTreeModel *model; + GtkTreePath *path; + GtkTreeViewDropPosition position; + GossipContact *contact; + GList *groups; + const gchar *id; + gchar *old_group; + gboolean is_row; + gboolean drag_success = TRUE; + gboolean drag_del = FALSE; + + priv = GET_PRIV (widget); + + id = (const gchar*) selection->data; + gossip_debug (DEBUG_DOMAIN, "Received %s%s drag & drop contact from roster with id:'%s'", + context->action == GDK_ACTION_MOVE ? "move" : "", + context->action == GDK_ACTION_COPY ? "copy" : "", + id); + + /* FIXME: This is ambigous, an id can come from multiple accounts */ + contact = empathy_contact_manager_find (priv->manager, id); + if (!contact) { + gossip_debug (DEBUG_DOMAIN, "No contact found associated with drag & drop"); + return; + } + + groups = gossip_contact_get_groups (contact); + + is_row = gtk_tree_view_get_dest_row_at_pos (GTK_TREE_VIEW (widget), + x, + y, + &path, + &position); + + if (!is_row) { + if (g_list_length (groups) != 1) { + /* if they have dragged a contact out of a + * group then we would set the contact to have + * NO groups but only if they were ONE group + * to begin with - should we do this + * regardless to how many groups they are in + * already or not at all? + */ + return; + } + + gossip_contact_set_groups (contact, NULL); + } else { + GList *l, *new_groups; + gchar *name; + gboolean is_group; + + model = gtk_tree_view_get_model (GTK_TREE_VIEW (widget)); + name = contact_list_get_parent_group (model, path, &is_group); + + if (groups && name && + g_list_find_custom (groups, name, (GCompareFunc)strcmp)) { + g_free (name); + return; + } + + /* Get source group information. */ + priv = GET_PRIV (widget); + if (!priv->drag_row) { + g_free (name); + return; + } + + path = gtk_tree_row_reference_get_path (priv->drag_row); + if (!path) { + g_free (name); + return; + } + + old_group = contact_list_get_parent_group (model, path, &is_group); + gtk_tree_path_free (path); + + if (!name && old_group && GDK_ACTION_MOVE) { + drag_success = FALSE; + } + + if (context->action == GDK_ACTION_MOVE) { + drag_del = TRUE; + } + + /* Create new groups GList. */ + for (l = groups, new_groups = NULL; l && drag_success; l = l->next) { + gchar *str; + + str = l->data; + if (context->action == GDK_ACTION_MOVE && + old_group != NULL && + strcmp (str, old_group) == 0) { + continue; + } + + if (str == NULL) { + continue; + } + + new_groups = g_list_append (new_groups, g_strdup (str)); + } + + if (drag_success) { + if (name) { + new_groups = g_list_append (new_groups, name); + } + gossip_contact_set_groups (contact, new_groups); + } else { + g_free (name); + } + } + + gtk_drag_finish (context, drag_success, drag_del, GDK_CURRENT_TIME); +} + +static gboolean +contact_list_drag_motion (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + guint time, + gpointer data) +{ + static DragMotionData *dm = NULL; + GtkTreePath *path; + gboolean is_row; + gboolean is_different = FALSE; + gboolean cleanup = TRUE; + + is_row = gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (widget), + x, + y, + &path, + NULL, + NULL, + NULL); + + cleanup &= (!dm); + + if (is_row) { + cleanup &= (dm && gtk_tree_path_compare (dm->path, path) != 0); + is_different = (!dm || (dm && gtk_tree_path_compare (dm->path, path) != 0)); + } else { + cleanup &= FALSE; + } + + if (!is_different && !cleanup) { + return TRUE; + } + + if (dm) { + gtk_tree_path_free (dm->path); + if (dm->timeout_id) { + g_source_remove (dm->timeout_id); + } + + g_free (dm); + + dm = NULL; + } + + if (!gtk_tree_view_row_expanded (GTK_TREE_VIEW (widget), path)) { + dm = g_new0 (DragMotionData, 1); + + dm->list = GOSSIP_CONTACT_LIST (widget); + dm->path = gtk_tree_path_copy (path); + + dm->timeout_id = g_timeout_add ( + 1500, + (GSourceFunc) contact_list_drag_motion_cb, + dm); + } + + return TRUE; +} + +static gboolean +contact_list_drag_motion_cb (DragMotionData *data) +{ + gtk_tree_view_expand_row (GTK_TREE_VIEW (data->list), + data->path, + FALSE); + + data->timeout_id = 0; + + return FALSE; +} + +static void +contact_list_drag_begin (GtkWidget *widget, + GdkDragContext *context, + gpointer user_data) +{ + GossipContactListPriv *priv; + GtkTreeSelection *selection; + GtkTreeModel *model; + GtkTreePath *path; + GtkTreeIter iter; + + priv = GET_PRIV (widget); + + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (widget)); + if (!gtk_tree_selection_get_selected (selection, &model, &iter)) { + return; + } + + path = gtk_tree_model_get_path (model, &iter); + priv->drag_row = gtk_tree_row_reference_new (model, path); + gtk_tree_path_free (path); +} + +static void +contact_list_drag_data_get (GtkWidget *widget, + GdkDragContext *context, + GtkSelectionData *selection, + guint info, + guint time, + gpointer user_data) +{ + GossipContactListPriv *priv; + GtkTreePath *src_path; + GtkTreeIter iter; + GtkTreeModel *model; + GossipContact *contact; + const gchar *id; + + priv = GET_PRIV (widget); + + model = gtk_tree_view_get_model (GTK_TREE_VIEW (widget)); + if (!priv->drag_row) { + return; + } + + src_path = gtk_tree_row_reference_get_path (priv->drag_row); + if (!src_path) { + return; + } + + if (!gtk_tree_model_get_iter (model, &iter, src_path)) { + gtk_tree_path_free (src_path); + return; + } + + gtk_tree_path_free (src_path); + + contact = gossip_contact_list_get_selected (GOSSIP_CONTACT_LIST (widget)); + if (!contact) { + return; + } + + id = gossip_contact_get_id (contact); + g_object_unref (contact); + + switch (info) { + case DND_DRAG_TYPE_CONTACT_ID: + gtk_selection_data_set (selection, drag_atoms_source[info], 8, + (guchar*)id, strlen (id) + 1); + break; + + default: + return; + } +} + +static void +contact_list_drag_end (GtkWidget *widget, + GdkDragContext *context, + gpointer user_data) +{ + GossipContactListPriv *priv; + + priv = GET_PRIV (widget); + + if (priv->drag_row) { + gtk_tree_row_reference_free (priv->drag_row); + priv->drag_row = NULL; + } +} + +static void +contact_list_cell_set_background (GossipContactList *list, + GtkCellRenderer *cell, + gboolean is_group, + gboolean is_active) +{ + GdkColor color; + GtkStyle *style; + gint color_sum_normal, color_sum_selected; + + g_return_if_fail (list != NULL); + g_return_if_fail (cell != NULL); + + style = gtk_widget_get_style (GTK_WIDGET (list)); + + if (!is_group) { + if (is_active) { + color = style->bg[GTK_STATE_SELECTED]; + + /* Here we take the current theme colour and add it to + * the colour for white and average the two. This + * gives a colour which is inline with the theme but + * slightly whiter. + */ + color.red = (color.red + (style->white).red) / 2; + color.green = (color.green + (style->white).green) / 2; + color.blue = (color.blue + (style->white).blue) / 2; + + g_object_set (cell, + "cell-background-gdk", &color, + NULL); + } else { + g_object_set (cell, + "cell-background-gdk", NULL, + NULL); + } + } else { + color = style->base[GTK_STATE_SELECTED]; + color_sum_normal = color.red+color.green+color.blue; + color = style->base[GTK_STATE_NORMAL]; + color_sum_selected = color.red+color.green+color.blue; + color = style->text_aa[GTK_STATE_INSENSITIVE]; + + if(color_sum_normal < color_sum_selected) { + /* found a light theme */ + color.red = (color.red + (style->white).red) / 2; + color.green = (color.green + (style->white).green) / 2; + color.blue = (color.blue + (style->white).blue) / 2; + } else { + /* found a dark theme */ + color.red = (color.red + (style->black).red) / 2; + color.green = (color.green + (style->black).green) / 2; + color.blue = (color.blue + (style->black).blue) / 2; + } + + g_object_set (cell, + "cell-background-gdk", &color, + NULL); + } +} + +static void +contact_list_pixbuf_cell_data_func (GtkTreeViewColumn *tree_column, + GtkCellRenderer *cell, + GtkTreeModel *model, + GtkTreeIter *iter, + GossipContactList *list) +{ + GdkPixbuf *pixbuf; + gboolean is_group; + gboolean is_active; + + gtk_tree_model_get (model, iter, + COL_IS_GROUP, &is_group, + COL_IS_ACTIVE, &is_active, + COL_PIXBUF_STATUS, &pixbuf, + -1); + + g_object_set (cell, + "visible", !is_group, + "pixbuf", pixbuf, + NULL); + + if (pixbuf) { + g_object_unref (pixbuf); + } + + contact_list_cell_set_background (list, cell, is_group, is_active); +} + +static void +contact_list_avatar_cell_data_func (GtkTreeViewColumn *tree_column, + GtkCellRenderer *cell, + GtkTreeModel *model, + GtkTreeIter *iter, + GossipContactList *list) +{ + GdkPixbuf *pixbuf; + gboolean show_avatar; + gboolean is_group; + gboolean is_active; + + gtk_tree_model_get (model, iter, + COL_PIXBUF_AVATAR, &pixbuf, + COL_PIXBUF_AVATAR_VISIBLE, &show_avatar, + COL_IS_GROUP, &is_group, + COL_IS_ACTIVE, &is_active, + -1); + + g_object_set (cell, + "visible", !is_group && show_avatar, + "pixbuf", pixbuf, + NULL); + + if (pixbuf) { + g_object_unref (pixbuf); + } + + contact_list_cell_set_background (list, cell, is_group, is_active); +} + +static void +contact_list_text_cell_data_func (GtkTreeViewColumn *tree_column, + GtkCellRenderer *cell, + GtkTreeModel *model, + GtkTreeIter *iter, + GossipContactList *list) +{ + gboolean is_group; + gboolean is_active; + gboolean show_status; + + gtk_tree_model_get (model, iter, + COL_IS_GROUP, &is_group, + COL_IS_ACTIVE, &is_active, + COL_STATUS_VISIBLE, &show_status, + -1); + + g_object_set (cell, + "show-status", show_status, + NULL); + + contact_list_cell_set_background (list, cell, is_group, is_active); +} + +static void +contact_list_expander_cell_data_func (GtkTreeViewColumn *column, + GtkCellRenderer *cell, + GtkTreeModel *model, + GtkTreeIter *iter, + GossipContactList *list) +{ + gboolean is_group; + gboolean is_active; + + gtk_tree_model_get (model, iter, + COL_IS_GROUP, &is_group, + COL_IS_ACTIVE, &is_active, + -1); + + if (gtk_tree_model_iter_has_child (model, iter)) { + GtkTreePath *path; + gboolean row_expanded; + + path = gtk_tree_model_get_path (model, iter); + row_expanded = gtk_tree_view_row_expanded (GTK_TREE_VIEW (column->tree_view), path); + gtk_tree_path_free (path); + + g_object_set (cell, + "visible", TRUE, + "expander-style", row_expanded ? GTK_EXPANDER_EXPANDED : GTK_EXPANDER_COLLAPSED, + NULL); + } else { + g_object_set (cell, "visible", FALSE, NULL); + } + + contact_list_cell_set_background (list, cell, is_group, is_active); +} + +static GtkWidget * +contact_list_get_contact_menu (GossipContactList *list, + gboolean can_send_file, + gboolean can_show_log) +{ + GossipContactListPriv *priv; + GtkAction *action; + GtkWidget *widget; + + priv = GET_PRIV (list); + + /* Sort out sensitive items */ + action = gtk_ui_manager_get_action (priv->ui, "/Contact/Log"); + gtk_action_set_sensitive (action, can_show_log); + + action = gtk_ui_manager_get_action (priv->ui, "/Contact/SendFile"); + gtk_action_set_visible (action, can_send_file); + + widget = gtk_ui_manager_get_widget (priv->ui, "/Contact"); + + return widget; +} + +GtkWidget * +gossip_contact_list_get_group_menu (GossipContactList *list) +{ + GossipContactListPriv *priv; + GtkWidget *widget; + + g_return_val_if_fail (GOSSIP_IS_CONTACT_LIST (list), NULL); + + priv = GET_PRIV (list); + + widget = gtk_ui_manager_get_widget (priv->ui, "/Group"); + + return widget; +} + +GtkWidget * +gossip_contact_list_get_contact_menu (GossipContactList *list, + GossipContact *contact) +{ + GtkWidget *menu; + gboolean can_show_log; + gboolean can_send_file; + + g_return_val_if_fail (GOSSIP_IS_CONTACT_LIST (list), NULL); + g_return_val_if_fail (GOSSIP_IS_CONTACT (contact), NULL); + + can_show_log = FALSE; /* FIXME: gossip_log_exists_for_contact (contact); */ + can_send_file = FALSE; + + menu = contact_list_get_contact_menu (list, + can_send_file, + can_show_log); + return menu; +} + +static gboolean +contact_list_button_press_event_cb (GossipContactList *list, + GdkEventButton *event, + gpointer user_data) +{ + GossipContactListPriv *priv; + GossipContact *contact; + GtkTreePath *path; + GtkTreeSelection *selection; + GtkTreeModel *model; + GtkTreeIter iter; + gboolean row_exists; + GtkWidget *menu; + + if (event->button != 3) { + return FALSE; + } + + priv = GET_PRIV (list); + + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (list)); + model = gtk_tree_view_get_model (GTK_TREE_VIEW (list)); + + gtk_widget_grab_focus (GTK_WIDGET (list)); + + row_exists = gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (list), + event->x, event->y, + &path, + NULL, NULL, NULL); + if (!row_exists) { + return FALSE; + } + + gtk_tree_selection_unselect_all (selection); + gtk_tree_selection_select_path (selection, path); + + gtk_tree_model_get_iter (model, &iter, path); + gtk_tree_path_free (path); + + gtk_tree_model_get (model, &iter, COL_CONTACT, &contact, -1); + + if (contact) { + menu = gossip_contact_list_get_contact_menu (list, contact); + g_object_unref (contact); + } else { + menu = gossip_contact_list_get_group_menu (list); + } + + if (!menu) { + return FALSE; + } + + gtk_widget_show (menu); + + gtk_menu_popup (GTK_MENU (menu), + NULL, NULL, NULL, NULL, + event->button, event->time); + + return TRUE; +} + +static void +contact_list_row_activated_cb (GossipContactList *list, + GtkTreePath *path, + GtkTreeViewColumn *col, + gpointer user_data) +{ + GossipContact *contact; + GtkTreeView *view; + GtkTreeModel *model; + GtkTreeIter iter; + + view = GTK_TREE_VIEW (list); + model = gtk_tree_view_get_model (view); + + gtk_tree_model_get_iter (model, &iter, path); + gtk_tree_model_get (model, &iter, COL_CONTACT, &contact, -1); + + if (contact) { + g_signal_emit (list, signals[CONTACT_CHAT], 0, contact); + g_object_unref (contact); + } +} + +static void +contact_list_row_expand_or_collapse_cb (GossipContactList *list, + GtkTreeIter *iter, + GtkTreePath *path, + gpointer user_data) +{ + GtkTreeModel *model; + gchar *name; + gboolean expanded; + + model = gtk_tree_view_get_model (GTK_TREE_VIEW (list)); + + gtk_tree_model_get (model, iter, + COL_NAME, &name, + -1); + + expanded = GPOINTER_TO_INT (user_data); + gossip_contact_group_set_expanded (name, expanded); + + g_free (name); +} + +static gint +contact_list_sort_func (GtkTreeModel *model, + GtkTreeIter *iter_a, + GtkTreeIter *iter_b, + gpointer user_data) +{ + gchar *name_a, *name_b; + GossipContact *contact_a, *contact_b; + gint ret_val; + + gtk_tree_model_get (model, iter_a, + COL_NAME, &name_a, + COL_CONTACT, &contact_a, + -1); + gtk_tree_model_get (model, iter_b, + COL_NAME, &name_b, + COL_CONTACT, &contact_b, + -1); + + /* If contact is NULL it means it's a group. */ + + if (!contact_a && contact_b) { + ret_val = 1; + } else if (contact_a && !contact_b) { + ret_val = -1; + } else { + ret_val = g_utf8_collate (name_a, name_b); + } + + g_free (name_a); + g_free (name_b); + + if (contact_a) { + g_object_unref (contact_a); + } + + if (contact_b) { + g_object_unref (contact_b); + } + + return ret_val; +} + +static gboolean +contact_list_iter_equal_contact (GtkTreeModel *model, + GtkTreeIter *iter, + GossipContact *contact) +{ + GossipContact *c; + gboolean equal; + + gtk_tree_model_get (model, iter, + COL_CONTACT, &c, + -1); + + if (!c) { + return FALSE; + } + + equal = (c == contact); + g_object_unref (c); + + return equal; +} + +static GList * +contact_list_find_contact (GossipContactList *list, + GossipContact *contact) +{ + GtkTreeModel *model; + FindContact fc; + GList *l = NULL; + + memset (&fc, 0, sizeof (fc)); + + fc.contact = contact; + + model = gtk_tree_view_get_model (GTK_TREE_VIEW (list)); + gtk_tree_model_foreach (model, + (GtkTreeModelForeachFunc) contact_list_find_contact_foreach, + &fc); + + if (fc.found) { + l = fc.iters; + } + + return l; +} + +static gboolean +contact_list_find_contact_foreach (GtkTreeModel *model, + GtkTreePath *path, + GtkTreeIter *iter, + FindContact *fc) +{ + if (contact_list_iter_equal_contact (model, iter, fc->contact)) { + fc->found = TRUE; + fc->iters = g_list_append (fc->iters, gtk_tree_iter_copy (iter)); + } + + /* We want to find ALL contacts that match, this means if we + * have the same contact in 3 groups, all iters should be + * returned. + */ + return FALSE; +} + +static void +contact_list_action_cb (GtkAction *action, + GossipContactList *list) +{ + GossipContact *contact; + const gchar *name; + gchar *group; + + name = gtk_action_get_name (action); + if (!name) { + return; + } + + gossip_debug (DEBUG_DOMAIN, "Action:'%s' activated", name); + + contact = gossip_contact_list_get_selected (list); + group = gossip_contact_list_get_selected_group (list); + + if (contact && strcmp (name, "Chat") == 0) { + g_signal_emit (list, signals[CONTACT_CHAT], 0, contact); + } + else if (contact && strcmp (name, "Information") == 0) { + g_signal_emit (list, signals[CONTACT_INFORMATION], 0, contact); + } + else if (contact && strcmp (name, "Edit") == 0) { + g_signal_emit (list, signals[CONTACT_EDIT], 0, contact); + } + else if (contact && strcmp (name, "Remove") == 0) { + g_signal_emit (list, signals[CONTACT_REMOVE], 0, contact); + } + else if (contact && strcmp (name, "Invite") == 0) { + g_signal_emit (list, signals[CONTACT_INVITE], 0, contact); + } + else if (contact && strcmp (name, "SendFile") == 0) { + g_signal_emit (list, signals[CONTACT_SEND_FILE], 0, contact); + } + else if (contact && strcmp (name, "Log") == 0) { + g_signal_emit (list, signals[CONTACT_LOG], 0, contact); + } + else if (group && strcmp (name, "Rename") == 0) { + g_signal_emit (list, signals[GROUP_RENAME], 0, group); + } + + g_free (group); + if (contact) { + g_object_unref (contact); + } +} + +static gboolean +contact_list_update_list_mode_foreach (GtkTreeModel *model, + GtkTreePath *path, + GtkTreeIter *iter, + GossipContactList *list) +{ + GossipContactListPriv *priv; + gboolean show_avatar = FALSE; + + priv = GET_PRIV (list); + + if (priv->show_avatars && !priv->is_compact) { + show_avatar = TRUE; + } + + gtk_tree_store_set (GTK_TREE_STORE (model), iter, + COL_PIXBUF_AVATAR_VISIBLE, show_avatar, + COL_STATUS_VISIBLE, !priv->is_compact, + -1); + + return FALSE; +} + +GossipContactList * +gossip_contact_list_new (void) +{ + return g_object_new (GOSSIP_TYPE_CONTACT_LIST, NULL); +} + +GossipContact * +gossip_contact_list_get_selected (GossipContactList *list) +{ + GossipContactListPriv *priv; + GtkTreeSelection *selection; + GtkTreeIter iter; + GtkTreeModel *model; + GossipContact *contact; + + g_return_val_if_fail (GOSSIP_IS_CONTACT_LIST (list), NULL); + + priv = GET_PRIV (list); + + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (list)); + if (!gtk_tree_selection_get_selected (selection, &model, &iter)) { + return NULL; + } + + gtk_tree_model_get (model, &iter, COL_CONTACT, &contact, -1); + + return contact; +} + +gchar * +gossip_contact_list_get_selected_group (GossipContactList *list) +{ + GossipContactListPriv *priv; + GtkTreeSelection *selection; + GtkTreeIter iter; + GtkTreeModel *model; + gboolean is_group; + gchar *name; + + g_return_val_if_fail (GOSSIP_IS_CONTACT_LIST (list), NULL); + + priv = GET_PRIV (list); + + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (list)); + if (!gtk_tree_selection_get_selected (selection, &model, &iter)) { + return NULL; + } + + gtk_tree_model_get (model, &iter, + COL_IS_GROUP, &is_group, + COL_NAME, &name, + -1); + + if (!is_group) { + g_free (name); + return NULL; + } + + return name; +} + +gboolean +gossip_contact_list_get_show_offline (GossipContactList *list) +{ + GossipContactListPriv *priv; + + g_return_val_if_fail (GOSSIP_IS_CONTACT_LIST (list), FALSE); + + priv = GET_PRIV (list); + + return priv->show_offline; +} + +void +gossip_contact_list_set_show_offline (GossipContactList *list, + gboolean show_offline) +{ + GossipContactListPriv *priv; + GList *contacts, *l; + gboolean show_active; + + g_return_if_fail (GOSSIP_IS_CONTACT_LIST (list)); + + priv = GET_PRIV (list); + + priv->show_offline = show_offline; + show_active = priv->show_active; + + /* Disable temporarily. */ + priv->show_active = FALSE; + + contacts = empathy_contact_manager_get_contacts (priv->manager); + for (l = contacts; l; l = l->next) { + GossipContact *contact; + + contact = GOSSIP_CONTACT (l->data); + + contact_list_contact_update (list, contact); + + g_object_unref (contact); + } + g_list_free (contacts); + + /* Restore to original setting. */ + priv->show_active = show_active; +} + +gboolean +gossip_contact_list_get_show_avatars (GossipContactList *list) +{ + GossipContactListPriv *priv; + + g_return_val_if_fail (GOSSIP_IS_CONTACT_LIST (list), TRUE); + + priv = GET_PRIV (list); + + return priv->show_avatars; +} + +void +gossip_contact_list_set_show_avatars (GossipContactList *list, + gboolean show_avatars) +{ + GossipContactListPriv *priv; + GtkTreeModel *model; + + g_return_if_fail (GOSSIP_IS_CONTACT_LIST (list)); + + priv = GET_PRIV (list); + + priv->show_avatars = show_avatars; + + model = gtk_tree_view_get_model (GTK_TREE_VIEW (list)); + + gtk_tree_model_foreach (model, + (GtkTreeModelForeachFunc) + contact_list_update_list_mode_foreach, + list); +} + +gboolean +gossip_contact_list_get_is_compact (GossipContactList *list) +{ + GossipContactListPriv *priv; + + g_return_val_if_fail (GOSSIP_IS_CONTACT_LIST (list), TRUE); + + priv = GET_PRIV (list); + + return priv->is_compact; +} + +void +gossip_contact_list_set_is_compact (GossipContactList *list, + gboolean is_compact) +{ + GossipContactListPriv *priv; + GtkTreeModel *model; + + g_return_if_fail (GOSSIP_IS_CONTACT_LIST (list)); + + priv = GET_PRIV (list); + + priv->is_compact = is_compact; + + model = gtk_tree_view_get_model (GTK_TREE_VIEW (list)); + + gtk_tree_model_foreach (model, + (GtkTreeModelForeachFunc) + contact_list_update_list_mode_foreach, + list); +} + diff --git a/libempathy-gtk/gossip-contact-list.h b/libempathy-gtk/gossip-contact-list.h new file mode 100644 index 00000000..b5947b94 --- /dev/null +++ b/libempathy-gtk/gossip-contact-list.h @@ -0,0 +1,72 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2004-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Mikael Hallendal <micke@imendio.com> + * Martyn Russell <martyn@imendio.com> + */ + +#ifndef __GOSSIP_CONTACT_LIST_H__ +#define __GOSSIP_CONTACT_LIST_H__ + +#include <gtk/gtktreeview.h> + +#include <libempathy/gossip-contact.h> + +G_BEGIN_DECLS + +#define GOSSIP_TYPE_CONTACT_LIST (gossip_contact_list_get_type ()) +#define GOSSIP_CONTACT_LIST(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GOSSIP_TYPE_CONTACT_LIST, GossipContactList)) +#define GOSSIP_CONTACT_LIST_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GOSSIP_TYPE_CONTACT_LIST, GossipContactListClass)) +#define GOSSIP_IS_CONTACT_LIST(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GOSSIP_TYPE_CONTACT_LIST)) +#define GOSSIP_IS_CONTACT_LIST_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GOSSIP_TYPE_CONTACT_LIST)) +#define GOSSIP_CONTACT_LIST_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GOSSIP_TYPE_CONTACT_LIST, GossipContactListClass)) + +typedef struct _GossipContactList GossipContactList; +typedef struct _GossipContactListClass GossipContactListClass; +typedef struct _GossipContactListPriv GossipContactListPriv; + +struct _GossipContactList { + GtkTreeView parent; +}; + +struct _GossipContactListClass { + GtkTreeViewClass parent_class; +}; + +GType gossip_contact_list_get_type (void) G_GNUC_CONST; +GossipContactList *gossip_contact_list_new (void); +GossipContact * gossip_contact_list_get_selected (GossipContactList *list); +gchar * gossip_contact_list_get_selected_group (GossipContactList *list); +gboolean gossip_contact_list_get_show_offline (GossipContactList *list); +void gossip_contact_list_set_show_offline (GossipContactList *list, + gboolean show_offline); +gboolean gossip_contact_list_get_show_avatars (GossipContactList *list); +void gossip_contact_list_set_show_avatars (GossipContactList *list, + gboolean show_avatars); +gboolean gossip_contact_list_get_is_compact (GossipContactList *list); +void gossip_contact_list_set_is_compact (GossipContactList *list, + gboolean is_compact); +GtkWidget * gossip_contact_list_get_contact_menu (GossipContactList *list, + GossipContact *contact); +GtkWidget * gossip_contact_list_get_group_menu (GossipContactList *list); + +G_END_DECLS + +#endif /* __GOSSIP_CONTACT_LIST_H__ */ + diff --git a/libempathy-gtk/gossip-preferences.c b/libempathy-gtk/gossip-preferences.c new file mode 100644 index 00000000..2f127a51 --- /dev/null +++ b/libempathy-gtk/gossip-preferences.c @@ -0,0 +1,885 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2003-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Mikael Hallendal <micke@imendio.com> + * Richard Hult <richard@imendio.com> + * Martyn Russell <martyn@imendio.com> + */ + +#include "config.h" + +#include <string.h> + +#include <gtk/gtk.h> +#include <glade/glade.h> +#include <glib/gi18n.h> + +#include <libempathy/gossip-conf.h> + +#include "gossip-preferences.h" +#include "gossip-stock.h" +#include "gossip-ui-utils.h" +#include "gossip-theme-manager.h" +#include "gossip-spell.h" + +typedef struct { + GtkWidget *dialog; + + GtkWidget *notebook; + + GtkWidget *checkbutton_show_avatars; + GtkWidget *checkbutton_compact_contact_list; + GtkWidget *checkbutton_show_smileys; + GtkWidget *combobox_chat_theme; + GtkWidget *checkbutton_theme_chat_room; + GtkWidget *checkbutton_separate_chat_windows; + + GtkWidget *checkbutton_sounds_for_messages; + GtkWidget *checkbutton_sounds_when_busy; + GtkWidget *checkbutton_sounds_when_away; + GtkWidget *checkbutton_popups_when_available; + + GtkWidget *treeview_spell_checker; + GtkWidget *checkbutton_spell_checker; + + GList *notify_ids; +} GossipPreferences; + +static void preferences_setup_widgets (GossipPreferences *preferences); +static void preferences_languages_setup (GossipPreferences *preferences); +static void preferences_languages_add (GossipPreferences *preferences); +static void preferences_languages_save (GossipPreferences *preferences); +static gboolean preferences_languages_save_foreach (GtkTreeModel *model, + GtkTreePath *path, + GtkTreeIter *iter, + gchar **languages); +static void preferences_languages_load (GossipPreferences *preferences); +static gboolean preferences_languages_load_foreach (GtkTreeModel *model, + GtkTreePath *path, + GtkTreeIter *iter, + gchar **languages); +static void preferences_languages_cell_toggled_cb (GtkCellRendererToggle *cell, + gchar *path_string, + GossipPreferences *preferences); +static void preferences_themes_setup (GossipPreferences *preferences); +static void preferences_widget_sync_bool (const gchar *key, + GtkWidget *widget); +static void preferences_widget_sync_int (const gchar *key, + GtkWidget *widget); +static void preferences_widget_sync_string (const gchar *key, + GtkWidget *widget); +static void preferences_widget_sync_string_combo (const gchar *key, + GtkWidget *widget); +static void preferences_notify_int_cb (GossipConf *conf, + const gchar *key, + gpointer user_data); +static void preferences_notify_string_cb (GossipConf *conf, + const gchar *key, + gpointer user_data); +static void preferences_notify_string_combo_cb (GossipConf *conf, + const gchar *key, + gpointer user_data); +static void preferences_notify_bool_cb (GossipConf *conf, + const gchar *key, + gpointer user_data); +static void preferences_notify_sensitivity_cb (GossipConf *conf, + const gchar *key, + gpointer user_data); +static void preferences_hookup_spin_button (GossipPreferences *preferences, + const gchar *key, + GtkWidget *widget); +static void preferences_hookup_entry (GossipPreferences *preferences, + const gchar *key, + GtkWidget *widget); +static void preferences_hookup_toggle_button (GossipPreferences *preferences, + const gchar *key, + GtkWidget *widget); +static void preferences_hookup_string_combo (GossipPreferences *preferences, + const gchar *key, + GtkWidget *widget); +static void preferences_hookup_sensitivity (GossipPreferences *preferences, + const gchar *key, + GtkWidget *widget); +static void preferences_spin_button_value_changed_cb (GtkWidget *button, + gpointer user_data); +static void preferences_entry_value_changed_cb (GtkWidget *entry, + gpointer user_data); +static void preferences_toggle_button_toggled_cb (GtkWidget *button, + gpointer user_data); +static void preferences_string_combo_changed_cb (GtkWidget *button, + gpointer user_data); +static void preferences_destroy_cb (GtkWidget *widget, + GossipPreferences *preferences); +static void preferences_response_cb (GtkWidget *widget, + gint response, + GossipPreferences *preferences); + +enum { + COL_LANG_ENABLED, + COL_LANG_CODE, + COL_LANG_NAME, + COL_LANG_COUNT +}; + +enum { + COL_COMBO_VISIBLE_NAME, + COL_COMBO_NAME, + COL_COMBO_COUNT +}; + +static void +preferences_setup_widgets (GossipPreferences *preferences) +{ + preferences_hookup_toggle_button (preferences, + GOSSIP_PREFS_SOUNDS_FOR_MESSAGES, + preferences->checkbutton_sounds_for_messages); + preferences_hookup_toggle_button (preferences, + GOSSIP_PREFS_SOUNDS_WHEN_AWAY, + preferences->checkbutton_sounds_when_away); + preferences_hookup_toggle_button (preferences, + GOSSIP_PREFS_SOUNDS_WHEN_BUSY, + preferences->checkbutton_sounds_when_busy); + preferences_hookup_toggle_button (preferences, + GOSSIP_PREFS_POPUPS_WHEN_AVAILABLE, + preferences->checkbutton_popups_when_available); + + preferences_hookup_sensitivity (preferences, + GOSSIP_PREFS_SOUNDS_FOR_MESSAGES, + preferences->checkbutton_sounds_when_away); + preferences_hookup_sensitivity (preferences, + GOSSIP_PREFS_SOUNDS_FOR_MESSAGES, + preferences->checkbutton_sounds_when_busy); + + preferences_hookup_toggle_button (preferences, + GOSSIP_PREFS_UI_SEPARATE_CHAT_WINDOWS, + preferences->checkbutton_separate_chat_windows); + + preferences_hookup_toggle_button (preferences, + GOSSIP_PREFS_UI_SHOW_AVATARS, + preferences->checkbutton_show_avatars); + + preferences_hookup_toggle_button (preferences, + GOSSIP_PREFS_UI_COMPACT_CONTACT_LIST, + preferences->checkbutton_compact_contact_list); + + preferences_hookup_toggle_button (preferences, + GOSSIP_PREFS_CHAT_SHOW_SMILEYS, + preferences->checkbutton_show_smileys); + + preferences_hookup_string_combo (preferences, + GOSSIP_PREFS_CHAT_THEME, + preferences->combobox_chat_theme); + + preferences_hookup_toggle_button (preferences, + GOSSIP_PREFS_CHAT_THEME_CHAT_ROOM, + preferences->checkbutton_theme_chat_room); + + preferences_hookup_toggle_button (preferences, + GOSSIP_PREFS_CHAT_SPELL_CHECKER_ENABLED, + preferences->checkbutton_spell_checker); + preferences_hookup_sensitivity (preferences, + GOSSIP_PREFS_CHAT_SPELL_CHECKER_ENABLED, + preferences->treeview_spell_checker); +} + +static void +preferences_languages_setup (GossipPreferences *preferences) +{ + GtkTreeView *view; + GtkListStore *store; + GtkTreeSelection *selection; + GtkTreeModel *model; + GtkTreeViewColumn *column; + GtkCellRenderer *renderer; + guint col_offset; + + view = GTK_TREE_VIEW (preferences->treeview_spell_checker); + + store = gtk_list_store_new (COL_LANG_COUNT, + G_TYPE_BOOLEAN, /* enabled */ + G_TYPE_STRING, /* code */ + G_TYPE_STRING); /* name */ + + gtk_tree_view_set_model (view, GTK_TREE_MODEL (store)); + + selection = gtk_tree_view_get_selection (view); + gtk_tree_selection_set_mode (selection, GTK_SELECTION_SINGLE); + + model = GTK_TREE_MODEL (store); + + renderer = gtk_cell_renderer_toggle_new (); + g_signal_connect (renderer, "toggled", + G_CALLBACK (preferences_languages_cell_toggled_cb), + preferences); + + column = gtk_tree_view_column_new_with_attributes (NULL, renderer, + "active", COL_LANG_ENABLED, + NULL); + + gtk_tree_view_append_column (view, column); + + renderer = gtk_cell_renderer_text_new (); + col_offset = gtk_tree_view_insert_column_with_attributes (view, + -1, _("Language"), + renderer, + "text", COL_LANG_NAME, + NULL); + + g_object_set_data (G_OBJECT (renderer), + "column", GINT_TO_POINTER (COL_LANG_NAME)); + + column = gtk_tree_view_get_column (view, col_offset - 1); + gtk_tree_view_column_set_sort_column_id (column, COL_LANG_NAME); + gtk_tree_view_column_set_resizable (column, FALSE); + gtk_tree_view_column_set_clickable (GTK_TREE_VIEW_COLUMN (column), TRUE); + + g_object_unref (store); +} + +static void +preferences_languages_add (GossipPreferences *preferences) +{ + GtkTreeView *view; + GtkListStore *store; + GList *codes, *l; + + view = GTK_TREE_VIEW (preferences->treeview_spell_checker); + store = GTK_LIST_STORE (gtk_tree_view_get_model (view)); + + codes = gossip_spell_get_language_codes (); + for (l = codes; l; l = l->next) { + GtkTreeIter iter; + const gchar *code; + const gchar *name; + + code = l->data; + name = gossip_spell_get_language_name (code); + if (!name) { + continue; + } + + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, + COL_LANG_CODE, code, + COL_LANG_NAME, name, + -1); + } + + gossip_spell_free_language_codes (codes); +} + +static void +preferences_languages_save (GossipPreferences *preferences) +{ + GtkTreeView *view; + GtkTreeModel *model; + + gchar *languages = NULL; + + view = GTK_TREE_VIEW (preferences->treeview_spell_checker); + model = gtk_tree_view_get_model (view); + + gtk_tree_model_foreach (model, + (GtkTreeModelForeachFunc) preferences_languages_save_foreach, + &languages); + + if (!languages) { + /* Default to english */ + languages = g_strdup ("en"); + } + + gossip_conf_set_string (gossip_conf_get (), + GOSSIP_PREFS_CHAT_SPELL_CHECKER_LANGUAGES, + languages); + + g_free (languages); +} + +static gboolean +preferences_languages_save_foreach (GtkTreeModel *model, + GtkTreePath *path, + GtkTreeIter *iter, + gchar **languages) +{ + gboolean enabled; + gchar *code; + + if (!languages) { + return TRUE; + } + + gtk_tree_model_get (model, iter, COL_LANG_ENABLED, &enabled, -1); + if (!enabled) { + return FALSE; + } + + gtk_tree_model_get (model, iter, COL_LANG_CODE, &code, -1); + if (!code) { + return FALSE; + } + + if (!(*languages)) { + *languages = g_strdup (code); + } else { + gchar *str = *languages; + *languages = g_strdup_printf ("%s,%s", str, code); + g_free (str); + } + + g_free (code); + + return FALSE; +} + +static void +preferences_languages_load (GossipPreferences *preferences) +{ + GtkTreeView *view; + GtkTreeModel *model; + gchar *value; + gchar **vlanguages; + + if (!gossip_conf_get_string (gossip_conf_get (), + GOSSIP_PREFS_CHAT_SPELL_CHECKER_LANGUAGES, + &value) || !value) { + return; + } + + vlanguages = g_strsplit (value, ",", -1); + g_free (value); + + view = GTK_TREE_VIEW (preferences->treeview_spell_checker); + model = gtk_tree_view_get_model (view); + + gtk_tree_model_foreach (model, + (GtkTreeModelForeachFunc) preferences_languages_load_foreach, + vlanguages); + + g_strfreev (vlanguages); +} + +static gboolean +preferences_languages_load_foreach (GtkTreeModel *model, + GtkTreePath *path, + GtkTreeIter *iter, + gchar **languages) +{ + gchar *code; + gchar *lang; + gint i; + gboolean found = FALSE; + + if (!languages) { + return TRUE; + } + + gtk_tree_model_get (model, iter, COL_LANG_CODE, &code, -1); + if (!code) { + return FALSE; + } + + for (i = 0, lang = languages[i]; lang; lang = languages[++i]) { + if (strcmp (lang, code) == 0) { + found = TRUE; + } + } + + gtk_list_store_set (GTK_LIST_STORE (model), iter, COL_LANG_ENABLED, found, -1); + return FALSE; +} + +static void +preferences_languages_cell_toggled_cb (GtkCellRendererToggle *cell, + gchar *path_string, + GossipPreferences *preferences) +{ + GtkTreeView *view; + GtkTreeModel *model; + GtkListStore *store; + GtkTreePath *path; + GtkTreeIter iter; + gboolean enabled; + + view = GTK_TREE_VIEW (preferences->treeview_spell_checker); + model = gtk_tree_view_get_model (view); + store = GTK_LIST_STORE (model); + + path = gtk_tree_path_new_from_string (path_string); + + gtk_tree_model_get_iter (model, &iter, path); + gtk_tree_model_get (model, &iter, COL_LANG_ENABLED, &enabled, -1); + + enabled ^= 1; + + gtk_list_store_set (store, &iter, COL_LANG_ENABLED, enabled, -1); + gtk_tree_path_free (path); + + preferences_languages_save (preferences); +} + +static void +preferences_themes_setup (GossipPreferences *preferences) +{ + GtkComboBox *combo; + GtkListStore *model; + GtkTreeIter iter; + const gchar **themes; + gint i; + + combo = GTK_COMBO_BOX (preferences->combobox_chat_theme); + + model = gtk_list_store_new (COL_COMBO_COUNT, + G_TYPE_STRING, + G_TYPE_STRING); + + themes = gossip_theme_manager_get_themes (); + for (i = 0; themes[i]; i += 2) { + gtk_list_store_append (model, &iter); + gtk_list_store_set (model, &iter, + COL_COMBO_VISIBLE_NAME, _(themes[i + 1]), + COL_COMBO_NAME, themes[i], + -1); + } + + gtk_combo_box_set_model (combo, GTK_TREE_MODEL (model)); + g_object_unref (model); +} + +static void +preferences_widget_sync_bool (const gchar *key, GtkWidget *widget) +{ + gboolean value; + + if (gossip_conf_get_bool (gossip_conf_get (), key, &value)) { + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget), value); + } +} + +static void +preferences_widget_sync_int (const gchar *key, GtkWidget *widget) +{ + gint value; + + if (gossip_conf_get_int (gossip_conf_get (), key, &value)) { + gtk_spin_button_set_value (GTK_SPIN_BUTTON (widget), value); + } +} + +static void +preferences_widget_sync_string (const gchar *key, GtkWidget *widget) +{ + gchar *value; + + if (gossip_conf_get_string (gossip_conf_get (), key, &value) && value) { + gtk_entry_set_text (GTK_ENTRY (widget), value); + g_free (value); + } +} + +static void +preferences_widget_sync_string_combo (const gchar *key, GtkWidget *widget) +{ + gchar *value; + GtkTreeModel *model; + GtkTreeIter iter; + gboolean found; + + if (!gossip_conf_get_string (gossip_conf_get (), key, &value)) { + return; + } + + model = gtk_combo_box_get_model (GTK_COMBO_BOX (widget)); + + found = FALSE; + if (value && gtk_tree_model_get_iter_first (model, &iter)) { + gchar *name; + + do { + gtk_tree_model_get (model, &iter, + COL_COMBO_NAME, &name, + -1); + + if (strcmp (name, value) == 0) { + found = TRUE; + gtk_combo_box_set_active_iter (GTK_COMBO_BOX (widget), &iter); + break; + } else { + found = FALSE; + } + + g_free (name); + } while (gtk_tree_model_iter_next (model, &iter)); + } + + /* Fallback to the first one. */ + if (!found) { + if (gtk_tree_model_get_iter_first (model, &iter)) { + gtk_combo_box_set_active_iter (GTK_COMBO_BOX (widget), &iter); + } + } + + g_free (value); +} + +static void +preferences_notify_int_cb (GossipConf *conf, + const gchar *key, + gpointer user_data) +{ + gint value; + + if (gossip_conf_get_int (conf, key, &value)) { + gtk_spin_button_set_value (GTK_SPIN_BUTTON (user_data), value); + } +} + +static void +preferences_notify_string_cb (GossipConf *conf, + const gchar *key, + gpointer user_data) +{ + gchar *value; + + if (gossip_conf_get_string (conf, key, &value) && value) { + gtk_entry_set_text (GTK_ENTRY (user_data), value); + g_free (value); + } +} + +static void +preferences_notify_string_combo_cb (GossipConf *conf, + const gchar *key, + gpointer user_data) +{ + preferences_widget_sync_string_combo (key, user_data); +} + +static void +preferences_notify_bool_cb (GossipConf *conf, + const gchar *key, + gpointer user_data) +{ + preferences_widget_sync_bool (key, user_data); +/* + if (gossip_conf_get_bool (conf, key, &value)) { + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (user_data), + gconf_value_get_bool (value)); + } +*/ +} + +static void +preferences_notify_sensitivity_cb (GossipConf *conf, + const gchar *key, + gpointer user_data) +{ + gboolean value; + + if (gossip_conf_get_bool (conf, key, &value)) { + gtk_widget_set_sensitive (GTK_WIDGET (user_data), value); + } +} + +static void +preferences_add_id (GossipPreferences *preferences, guint id) +{ + preferences->notify_ids = g_list_prepend (preferences->notify_ids, + GUINT_TO_POINTER (id)); +} + +static void +preferences_hookup_spin_button (GossipPreferences *preferences, + const gchar *key, + GtkWidget *widget) +{ + guint id; + + /* Silence warning. */ + if (0) { + preferences_hookup_spin_button (preferences, key, widget); + } + + preferences_widget_sync_int (key, widget); + + g_object_set_data_full (G_OBJECT (widget), "key", + g_strdup (key), g_free); + + g_signal_connect (widget, + "value_changed", + G_CALLBACK (preferences_spin_button_value_changed_cb), + NULL); + + id = gossip_conf_notify_add (gossip_conf_get (), + key, + preferences_notify_int_cb, + widget); + if (id) { + preferences_add_id (preferences, id); + } +} + +static void +preferences_hookup_entry (GossipPreferences *preferences, + const gchar *key, + GtkWidget *widget) +{ + guint id; + + if (0) { /* Silent warning before we use this function. */ + preferences_hookup_entry (preferences, key, widget); + } + + preferences_widget_sync_string (key, widget); + + g_object_set_data_full (G_OBJECT (widget), "key", + g_strdup (key), g_free); + + g_signal_connect (widget, + "changed", + G_CALLBACK (preferences_entry_value_changed_cb), + NULL); + + id = gossip_conf_notify_add (gossip_conf_get (), + key, + preferences_notify_string_cb, + widget); + if (id) { + preferences_add_id (preferences, id); + } +} + +static void +preferences_hookup_toggle_button (GossipPreferences *preferences, + const gchar *key, + GtkWidget *widget) +{ + guint id; + + preferences_widget_sync_bool (key, widget); + + g_object_set_data_full (G_OBJECT (widget), "key", + g_strdup (key), g_free); + + g_signal_connect (widget, + "toggled", + G_CALLBACK (preferences_toggle_button_toggled_cb), + NULL); + + id = gossip_conf_notify_add (gossip_conf_get (), + key, + preferences_notify_bool_cb, + widget); + if (id) { + preferences_add_id (preferences, id); + } +} + +static void +preferences_hookup_string_combo (GossipPreferences *preferences, + const gchar *key, + GtkWidget *widget) +{ + guint id; + + preferences_widget_sync_string_combo (key, widget); + + g_object_set_data_full (G_OBJECT (widget), "key", + g_strdup (key), g_free); + + g_signal_connect (widget, + "changed", + G_CALLBACK (preferences_string_combo_changed_cb), + NULL); + + id = gossip_conf_notify_add (gossip_conf_get (), + key, + preferences_notify_string_combo_cb, + widget); + if (id) { + preferences_add_id (preferences, id); + } +} + +static void +preferences_hookup_sensitivity (GossipPreferences *preferences, + const gchar *key, + GtkWidget *widget) +{ + gboolean value; + guint id; + + if (gossip_conf_get_bool (gossip_conf_get (), key, &value)) { + gtk_widget_set_sensitive (widget, value); + } + + id = gossip_conf_notify_add (gossip_conf_get (), + key, + preferences_notify_sensitivity_cb, + widget); + if (id) { + preferences_add_id (preferences, id); + } +} + +static void +preferences_spin_button_value_changed_cb (GtkWidget *button, + gpointer user_data) +{ + const gchar *key; + + key = g_object_get_data (G_OBJECT (button), "key"); + + gossip_conf_set_int (gossip_conf_get (), + key, + gtk_spin_button_get_value (GTK_SPIN_BUTTON (button))); +} + +static void +preferences_entry_value_changed_cb (GtkWidget *entry, + gpointer user_data) +{ + const gchar *key; + + key = g_object_get_data (G_OBJECT (entry), "key"); + + gossip_conf_set_string (gossip_conf_get (), + key, + gtk_entry_get_text (GTK_ENTRY (entry))); +} + +static void +preferences_toggle_button_toggled_cb (GtkWidget *button, + gpointer user_data) +{ + const gchar *key; + + key = g_object_get_data (G_OBJECT (button), "key"); + + gossip_conf_set_bool (gossip_conf_get (), + key, + gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button))); +} + +static void +preferences_string_combo_changed_cb (GtkWidget *combo, + gpointer user_data) +{ + const gchar *key; + GtkTreeModel *model; + GtkTreeIter iter; + gchar *name; + + key = g_object_get_data (G_OBJECT (combo), "key"); + + if (gtk_combo_box_get_active_iter (GTK_COMBO_BOX (combo), &iter)) { + model = gtk_combo_box_get_model (GTK_COMBO_BOX (combo)); + + gtk_tree_model_get (model, &iter, + COL_COMBO_NAME, &name, + -1); + gossip_conf_set_string (gossip_conf_get (), key, name); + g_free (name); + } +} + +static void +preferences_response_cb (GtkWidget *widget, + gint response, + GossipPreferences *preferences) +{ + gtk_widget_destroy (widget); +} + +static void +preferences_destroy_cb (GtkWidget *widget, + GossipPreferences *preferences) +{ + GList *l; + + for (l = preferences->notify_ids; l; l = l->next) { + guint id; + + id = GPOINTER_TO_UINT (l->data); + gossip_conf_notify_remove (gossip_conf_get (), id); + } + + g_list_free (preferences->notify_ids); + g_free (preferences); +} + +void +gossip_preferences_show (void) +{ + static GossipPreferences *preferences; + GladeXML *glade; + + if (preferences) { + gtk_window_present (GTK_WINDOW (preferences->dialog)); + return; + } + + preferences = g_new0 (GossipPreferences, 1); + + glade = gossip_glade_get_file ( + "main.glade", + "preferences_dialog", + NULL, + "preferences_dialog", &preferences->dialog, + "notebook", &preferences->notebook, + "checkbutton_show_avatars", &preferences->checkbutton_show_avatars, + "checkbutton_compact_contact_list", &preferences->checkbutton_compact_contact_list, + "checkbutton_show_smileys", &preferences->checkbutton_show_smileys, + "combobox_chat_theme", &preferences->combobox_chat_theme, + "checkbutton_theme_chat_room", &preferences->checkbutton_theme_chat_room, + "checkbutton_separate_chat_windows", &preferences->checkbutton_separate_chat_windows, + "checkbutton_sounds_for_messages", &preferences->checkbutton_sounds_for_messages, + "checkbutton_sounds_when_busy", &preferences->checkbutton_sounds_when_busy, + "checkbutton_sounds_when_away", &preferences->checkbutton_sounds_when_away, + "checkbutton_popups_when_available", &preferences->checkbutton_popups_when_available, + "treeview_spell_checker", &preferences->treeview_spell_checker, + "checkbutton_spell_checker", &preferences->checkbutton_spell_checker, + NULL); + + gossip_glade_connect (glade, + preferences, + "preferences_dialog", "destroy", preferences_destroy_cb, + "preferences_dialog", "response", preferences_response_cb, + NULL); + + g_object_unref (glade); + + g_object_add_weak_pointer (G_OBJECT (preferences->dialog), (gpointer) &preferences); + + preferences_themes_setup (preferences); + + preferences_setup_widgets (preferences); + + preferences_languages_setup (preferences); + preferences_languages_add (preferences); + preferences_languages_load (preferences); + + if (gossip_spell_supported ()) { + GtkWidget *page; + + page = gtk_notebook_get_nth_page (GTK_NOTEBOOK (preferences->notebook), 2); + gtk_widget_show (page); + } + + gtk_widget_show (preferences->dialog); +} diff --git a/libempathy-gtk/gossip-preferences.h b/libempathy-gtk/gossip-preferences.h new file mode 100644 index 00000000..428d0fc7 --- /dev/null +++ b/libempathy-gtk/gossip-preferences.h @@ -0,0 +1,55 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2003-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Mikael Hallendal <micke@imendio.com> + * Richard Hult <richard@imendio.com> + * Martyn Russell <martyn@imendio.com> + */ + +#ifndef __GOSSIP_PREFERENCES_H__ +#define __GOSSIP_PREFERENCES_H__ + +G_BEGIN_DECLS + +#define GOSSIP_PREFS_PATH "/apps/empathy" + +#define GOSSIP_PREFS_SOUNDS_FOR_MESSAGES GOSSIP_PREFS_PATH "/notifications/sounds_for_messages" +#define GOSSIP_PREFS_SOUNDS_WHEN_AWAY GOSSIP_PREFS_PATH "/notifications/sounds_when_away" +#define GOSSIP_PREFS_SOUNDS_WHEN_BUSY GOSSIP_PREFS_PATH "/notifications/sounds_when_busy" +#define GOSSIP_PREFS_POPUPS_WHEN_AVAILABLE GOSSIP_PREFS_PATH "/notifications/popups_when_available" +#define GOSSIP_PREFS_CHAT_SHOW_SMILEYS GOSSIP_PREFS_PATH "/conversation/graphical_smileys" +#define GOSSIP_PREFS_CHAT_THEME GOSSIP_PREFS_PATH "/conversation/theme" +#define GOSSIP_PREFS_CHAT_THEME_CHAT_ROOM GOSSIP_PREFS_PATH "/conversation/theme_chat_room" +#define GOSSIP_PREFS_CHAT_SPELL_CHECKER_LANGUAGES GOSSIP_PREFS_PATH "/conversation/spell_checker_languages" +#define GOSSIP_PREFS_CHAT_SPELL_CHECKER_ENABLED GOSSIP_PREFS_PATH "/conversation/spell_checker_enabled" +#define GOSSIP_PREFS_UI_SEPARATE_CHAT_WINDOWS GOSSIP_PREFS_PATH "/ui/separate_chat_windows" +#define GOSSIP_PREFS_UI_MAIN_WINDOW_HIDDEN GOSSIP_PREFS_PATH "/ui/main_window_hidden" +#define GOSSIP_PREFS_UI_AVATAR_DIRECTORY GOSSIP_PREFS_PATH "/ui/avatar_directory" +#define GOSSIP_PREFS_UI_SHOW_AVATARS GOSSIP_PREFS_PATH "/ui/show_avatars" +#define GOSSIP_PREFS_UI_COMPACT_CONTACT_LIST GOSSIP_PREFS_PATH "/ui/compact_contact_list" +#define GOSSIP_PREFS_CONTACTS_SHOW_OFFLINE GOSSIP_PREFS_PATH "/contacts/show_offline" +#define GOSSIP_PREFS_HINTS_CLOSE_MAIN_WINDOW GOSSIP_PREFS_PATH "/hints/close_main_window" + +void gossip_preferences_show (void); + +G_END_DECLS + +#endif /* __GOSSIP_PREFERENCES_H__ */ + + diff --git a/libempathy-gtk/gossip-private-chat.c b/libempathy-gtk/gossip-private-chat.c new file mode 100644 index 00000000..41a7f594 --- /dev/null +++ b/libempathy-gtk/gossip-private-chat.c @@ -0,0 +1,495 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2002-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Mikael Hallendal <micke@imendio.com> + * Richard Hult <richard@imendio.com> + * Martyn Russell <martyn@imendio.com> + * Geert-Jan Van den Bogaerde <geertjan@gnome.org> + */ + +#include "config.h" + +#include <string.h> + +#include <gtk/gtk.h> +#include <glade/glade.h> +#include <glib/gi18n.h> + +#include <libmissioncontrol/mc-account.h> + +#include <libempathy/gossip-debug.h> +#include <libempathy/empathy-tp-chat.h> +//#include <libgossip/gossip-log.h> + +#include "gossip-private-chat.h" +#include "gossip-chat-view.h" +#include "gossip-chat.h" +#include "gossip-preferences.h" +//#include "gossip-sound.h" +#include "gossip-stock.h" +#include "gossip-ui-utils.h" + +#define DEBUG_DOMAIN "PrivateChat" + +#define COMPOSING_STOP_TIMEOUT 5 + +#define GET_PRIV(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GOSSIP_TYPE_PRIVATE_CHAT, GossipPrivateChatPriv)) + +struct _GossipPrivateChatPriv { + GossipContact *contact; + gchar *name; + + guint scroll_idle_id; + gboolean is_online; + + GtkWidget *widget; + GtkWidget *text_view_sw; +}; + +static void gossip_private_chat_class_init (GossipPrivateChatClass *klass); +static void gossip_private_chat_init (GossipPrivateChat *chat); +static void private_chat_finalize (GObject *object); +static void private_chat_create_ui (GossipPrivateChat *chat); +static void private_chat_contact_presence_updated_cb (GossipContact *contact, + GParamSpec *param, + GossipPrivateChat *chat); +static void private_chat_contact_updated_cb (GossipContact *contact, + GParamSpec *param, + GossipPrivateChat *chat); +static void private_chat_widget_destroy_cb (GtkWidget *widget, + GossipPrivateChat *chat); +static const gchar * private_chat_get_name (GossipChat *chat); +static gchar * private_chat_get_tooltip (GossipChat *chat); +static GdkPixbuf * private_chat_get_status_pixbuf (GossipChat *chat); +static GossipContact *private_chat_get_contact (GossipChat *chat); +static GtkWidget * private_chat_get_widget (GossipChat *chat); +/*static GdkPixbuf * private_chat_pad_to_size (GdkPixbuf *pixbuf, + gint width, + gint height, + gint extra_padding_right);*/ + +G_DEFINE_TYPE (GossipPrivateChat, gossip_private_chat, GOSSIP_TYPE_CHAT); + +static void +gossip_private_chat_class_init (GossipPrivateChatClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GossipChatClass *chat_class = GOSSIP_CHAT_CLASS (klass); + + object_class->finalize = private_chat_finalize; + + chat_class->get_name = private_chat_get_name; + chat_class->get_tooltip = private_chat_get_tooltip; + chat_class->get_status_pixbuf = private_chat_get_status_pixbuf; + chat_class->get_contact = private_chat_get_contact; + chat_class->get_widget = private_chat_get_widget; + chat_class->get_show_contacts = NULL; + chat_class->set_show_contacts = NULL; + chat_class->is_group_chat = NULL; + + g_type_class_add_private (object_class, sizeof (GossipPrivateChatPriv)); +} + +static void +gossip_private_chat_init (GossipPrivateChat *chat) +{ + GossipPrivateChatPriv *priv; + + priv = GET_PRIV (chat); + + priv->is_online = FALSE; + + private_chat_create_ui (chat); + +} + +static void +private_chat_finalize (GObject *object) +{ + GossipPrivateChat *chat; + GossipPrivateChatPriv *priv; + + chat = GOSSIP_PRIVATE_CHAT (object); + priv = GET_PRIV (chat); + + g_signal_handlers_disconnect_by_func (priv->contact, + private_chat_contact_updated_cb, + chat); + g_signal_handlers_disconnect_by_func (priv->contact, + private_chat_contact_presence_updated_cb, + chat); + + if (priv->contact) { + g_object_unref (priv->contact); + } + + if (priv->scroll_idle_id) { + g_source_remove (priv->scroll_idle_id); + } + + g_free (priv->name); + + G_OBJECT_CLASS (gossip_private_chat_parent_class)->finalize (object); +} + +static void +private_chat_create_ui (GossipPrivateChat *chat) +{ + GladeXML *glade; + GossipPrivateChatPriv *priv; + GtkWidget *input_text_view_sw; + + priv = GET_PRIV (chat); + + glade = gossip_glade_get_file ("empathy-chat.glade", + "chat_widget", + NULL, + "chat_widget", &priv->widget, + "chat_view_sw", &priv->text_view_sw, + "input_text_view_sw", &input_text_view_sw, + NULL); + + gossip_glade_connect (glade, + chat, + "chat_widget", "destroy", private_chat_widget_destroy_cb, + NULL); + + g_object_unref (glade); + + g_object_set_data (G_OBJECT (priv->widget), "chat", g_object_ref (chat)); + + gtk_container_add (GTK_CONTAINER (priv->text_view_sw), + GTK_WIDGET (GOSSIP_CHAT (chat)->view)); + gtk_widget_show (GTK_WIDGET (GOSSIP_CHAT (chat)->view)); + + gtk_container_add (GTK_CONTAINER (input_text_view_sw), + GOSSIP_CHAT (chat)->input_text_view); + gtk_widget_show (GOSSIP_CHAT (chat)->input_text_view); +} + +static void +private_chat_contact_presence_updated_cb (GossipContact *contact, + GParamSpec *param, + GossipPrivateChat *chat) +{ + GossipPrivateChatPriv *priv; + + priv = GET_PRIV (chat); + + gossip_debug (DEBUG_DOMAIN, "Presence update for contact: %s", + gossip_contact_get_id (contact)); + + if (!gossip_contact_is_online (contact)) { + if (priv->is_online) { + gchar *msg; + + msg = g_strdup_printf (_("%s went offline"), + gossip_contact_get_name (priv->contact)); + gossip_chat_view_append_event (GOSSIP_CHAT (chat)->view, msg); + g_free (msg); + } + + priv->is_online = FALSE; + + g_signal_emit_by_name (chat, "composing", FALSE); + + } else { + if (!priv->is_online) { + gchar *msg; + + msg = g_strdup_printf (_("%s has come online"), + gossip_contact_get_name (priv->contact)); + gossip_chat_view_append_event (GOSSIP_CHAT (chat)->view, msg); + g_free (msg); + } + + priv->is_online = TRUE; + } + + g_signal_emit_by_name (chat, "status-changed"); +} + +static void +private_chat_contact_updated_cb (GossipContact *contact, + GParamSpec *param, + GossipPrivateChat *chat) +{ + GossipPrivateChatPriv *priv; + + priv = GET_PRIV (chat); + + if (strcmp (priv->name, gossip_contact_get_name (contact)) != 0) { + g_free (priv->name); + priv->name = g_strdup (gossip_contact_get_name (contact)); + g_signal_emit_by_name (chat, "name-changed", priv->name); + } +} + +static void +private_chat_widget_destroy_cb (GtkWidget *widget, + GossipPrivateChat *chat) +{ + gossip_debug (DEBUG_DOMAIN, "Destroyed"); + + g_object_unref (chat); +} + +static const gchar * +private_chat_get_name (GossipChat *chat) +{ + GossipPrivateChatPriv *priv; + + g_return_val_if_fail (GOSSIP_IS_PRIVATE_CHAT (chat), NULL); + + priv = GET_PRIV (chat); + + return priv->name; +} + +static gchar * +private_chat_get_tooltip (GossipChat *chat) +{ + GossipPrivateChatPriv *priv; + GossipContact *contact; + const gchar *status; + + g_return_val_if_fail (GOSSIP_IS_PRIVATE_CHAT (chat), NULL); + + priv = GET_PRIV (chat); + + contact = gossip_chat_get_contact (chat); + status = gossip_contact_get_status (contact); + + return g_strdup_printf ("%s\n%s", + gossip_contact_get_id (contact), + status); +} + +static GdkPixbuf * +private_chat_get_status_pixbuf (GossipChat *chat) +{ + GossipPrivateChatPriv *priv; + GossipContact *contact; + + g_return_val_if_fail (GOSSIP_IS_PRIVATE_CHAT (chat), NULL); + + priv = GET_PRIV (chat); + + contact = gossip_chat_get_contact (chat); + + return gossip_pixbuf_for_contact (contact); +} + +static GossipContact * +private_chat_get_contact (GossipChat *chat) +{ + GossipPrivateChatPriv *priv; + + g_return_val_if_fail (GOSSIP_IS_PRIVATE_CHAT (chat), NULL); + + priv = GET_PRIV (chat); + + return priv->contact; +} + +static GtkWidget * +private_chat_get_widget (GossipChat *chat) +{ + GossipPrivateChatPriv *priv; + + priv = GET_PRIV (chat); + + return priv->widget; +} + +/* Scroll down after the back-log has been received. */ +static gboolean +private_chat_scroll_down_idle_func (GossipChat *chat) +{ + GossipPrivateChatPriv *priv; + + priv = GET_PRIV (chat); + + gossip_chat_scroll_down (chat); + g_object_unref (chat); + + priv->scroll_idle_id = 0; + + return FALSE; +} + +static void +private_chat_setup (GossipPrivateChat *chat, + GossipContact *contact, + EmpathyTpChat *tp_chat) +{ + GossipPrivateChatPriv *priv; + //GossipLogManager *log_manager; + GossipChatView *view; +/* GossipContact *sender; + GossipMessage *message; + GList *messages, *l; + gint num_messages, i;*/ + + priv = GET_PRIV (chat); + + gossip_chat_set_tp_chat (GOSSIP_CHAT (chat), tp_chat); + + priv->contact = g_object_ref (contact); + GOSSIP_CHAT (chat)->account = g_object_ref (gossip_contact_get_account (contact)); + + priv->name = g_strdup (gossip_contact_get_name (contact)); + + g_signal_connect (priv->contact, + "notify::name", + G_CALLBACK (private_chat_contact_updated_cb), + chat); + g_signal_connect (priv->contact, + "notify::presences", + G_CALLBACK (private_chat_contact_presence_updated_cb), + chat); + + view = GOSSIP_CHAT (chat)->view; + + /* Turn off scrolling temporarily */ + gossip_chat_view_scroll (view, FALSE); +#if 0 +FIXME: + /* Add messages from last conversation */ + log_manager = gossip_session_get_log_manager (gossip_app_get_session ()); + messages = gossip_log_get_last_for_contact (log_manager, priv->contact); + num_messages = g_list_length (messages); + + for (l = messages, i = 0; l; l = l->next, i++) { + message = l->data; + + if (num_messages - i > 10) { + continue; + } + + sender = gossip_message_get_sender (message); + if (gossip_contact_equal (priv->own_contact, sender)) { + gossip_chat_view_append_message_from_self (view, + message, + priv->own_contact, + priv->own_avatar); + } else { + gossip_chat_view_append_message_from_other (view, + message, + sender, + priv->other_avatar); + } + } + + g_list_foreach (messages, (GFunc) g_object_unref, NULL); + g_list_free (messages); +#endif + /* Turn back on scrolling */ + gossip_chat_view_scroll (view, TRUE); + + /* Scroll to the most recent messages, we reference the chat + * for the duration of the scroll func. + */ + priv->scroll_idle_id = g_idle_add ((GSourceFunc) + private_chat_scroll_down_idle_func, + g_object_ref (chat)); + + priv->is_online = gossip_contact_is_online (priv->contact); +} + +GossipPrivateChat * +gossip_private_chat_new (GossipContact *contact) +{ + GossipPrivateChat *chat; + EmpathyTpChat *tp_chat; + + g_return_val_if_fail (GOSSIP_IS_CONTACT (contact), NULL); + + chat = g_object_new (GOSSIP_TYPE_PRIVATE_CHAT, NULL); + tp_chat = empathy_tp_chat_new_with_contact (contact); + + private_chat_setup (chat, contact, tp_chat); + g_object_unref (tp_chat); + + return chat; +} + +GossipPrivateChat * +gossip_private_chat_new_with_channel (GossipContact *contact, + TpChan *tp_chan) +{ + GossipPrivateChat *chat; + EmpathyTpChat *tp_chat; + McAccount *account; + + g_return_val_if_fail (GOSSIP_IS_CONTACT (contact), NULL); + g_return_val_if_fail (TELEPATHY_IS_CHAN (tp_chan), NULL); + + account = gossip_contact_get_account (contact); + chat = g_object_new (GOSSIP_TYPE_PRIVATE_CHAT, NULL); + tp_chat = empathy_tp_chat_new (account, tp_chan); + + private_chat_setup (chat, contact, tp_chat); + g_object_unref (tp_chat); + + return chat; +} + +/* Pads a pixbuf to the specified size, by centering it in a larger transparent + * pixbuf. Returns a new ref. + */ +#if 0 +FIXME: +static GdkPixbuf * +private_chat_pad_to_size (GdkPixbuf *pixbuf, + gint width, + gint height, + gint extra_padding_right) +{ + gint src_width, src_height; + GdkPixbuf *padded; + gint x_offset, y_offset; + + src_width = gdk_pixbuf_get_width (pixbuf); + src_height = gdk_pixbuf_get_height (pixbuf); + + x_offset = (width - src_width) / 2; + y_offset = (height - src_height) / 2; + + padded = gdk_pixbuf_new (gdk_pixbuf_get_colorspace (pixbuf), + TRUE, /* alpha */ + gdk_pixbuf_get_bits_per_sample (pixbuf), + width + extra_padding_right, + height); + + gdk_pixbuf_fill (padded, 0); + + gdk_pixbuf_copy_area (pixbuf, + 0, /* source coords */ + 0, + src_width, + src_height, + padded, + x_offset, /* dest coords */ + y_offset); + + return padded; +} +#endif + diff --git a/libempathy-gtk/gossip-private-chat.h b/libempathy-gtk/gossip-private-chat.h new file mode 100644 index 00000000..48741696 --- /dev/null +++ b/libempathy-gtk/gossip-private-chat.h @@ -0,0 +1,62 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2002-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Mikael Hallendal <micke@imendio.com> + * Richard Hult <richard@imendio.com> + * Martyn Russell <martyn@imendio.com> + * Geert-Jan Van den Bogaerde <geertjan@gnome.org> + */ + +#ifndef __GOSSIP_PRIVATE_CHAT_H__ +#define __GOSSIP_PRIVATE_CHAT_H__ + +#include <libempathy/gossip-contact.h> +#include <libempathy/gossip-message.h> + +G_BEGIN_DECLS + +#define GOSSIP_TYPE_PRIVATE_CHAT (gossip_private_chat_get_type ()) +#define GOSSIP_PRIVATE_CHAT(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GOSSIP_TYPE_PRIVATE_CHAT, GossipPrivateChat)) +#define GOSSIP_PRIVATE_CHAT_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), GOSSIP_TYPE_PRIVATE_CHAT, GossipPrivateChatClass)) +#define GOSSIP_IS_PRIVATE_CHAT(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GOSSIP_TYPE_PRIVATE_CHAT)) +#define GOSSIP_IS_PRIVATE_CHAT_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GOSSIP_TYPE_PRIVATE_CHAT)) +#define GOSSIP_PRIVATE_CHAT_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GOSSIP_TYPE_PRIVATE_CHAT, GossipPrivateChatClass)) + +typedef struct _GossipPrivateChat GossipPrivateChat; +typedef struct _GossipPrivateChatClass GossipPrivateChatClass; +typedef struct _GossipPrivateChatPriv GossipPrivateChatPriv; + +#include "gossip-chat.h" + +struct _GossipPrivateChat { + GossipChat parent; +}; + +struct _GossipPrivateChatClass { + GossipChatClass parent; +}; + +GType gossip_private_chat_get_type (void); +GossipPrivateChat * gossip_private_chat_new (GossipContact *contact); +GossipPrivateChat * gossip_private_chat_new_with_channel (GossipContact *contact, + TpChan *tp_chan); + +G_END_DECLS + +#endif /* __GOSSIP_PRIVATE_CHAT_H__ */ diff --git a/libempathy-gtk/gossip-profile-chooser.c b/libempathy-gtk/gossip-profile-chooser.c new file mode 100644 index 00000000..15f2fbb3 --- /dev/null +++ b/libempathy-gtk/gossip-profile-chooser.c @@ -0,0 +1,102 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2007 Xavier Claessens <xclaesse@gmail.com> + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include <config.h> + +#include <gtk/gtk.h> +#include <libmissioncontrol/mc-profile.h> + +#include <libempathy-gtk/gossip-ui-utils.h> + +#include "gossip-profile-chooser.h" + +enum { + COL_ICON, + COL_LABEL, + COL_PROFILE, + COL_COUNT +}; + +McProfile* +gossip_profile_chooser_get_selected (GtkWidget *widget) +{ + GtkTreeModel *model; + GtkTreeIter iter; + McProfile *profile = NULL; + + model = gtk_combo_box_get_model (GTK_COMBO_BOX (widget)); + if (gtk_combo_box_get_active_iter (GTK_COMBO_BOX (widget), &iter)) { + gtk_tree_model_get (model, &iter, + COL_PROFILE, &profile, + -1); + } + + return profile; +} + +GtkWidget * +gossip_profile_chooser_new (void) +{ + GList *profiles, *l; + GtkListStore *store; + GtkCellRenderer *renderer; + GtkWidget *combo_box; + GtkTreeIter iter; + + /* set up combo box with new store */ + store = gtk_list_store_new (COL_COUNT, + GDK_TYPE_PIXBUF, /* Icon */ + G_TYPE_STRING, /* Label */ + MC_TYPE_PROFILE); /* Profile */ + combo_box = gtk_combo_box_new_with_model (GTK_TREE_MODEL (store)); + + + renderer = gtk_cell_renderer_pixbuf_new (); + gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo_box), renderer, FALSE); + gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (combo_box), renderer, + "pixbuf", COL_ICON, + NULL); + renderer = gtk_cell_renderer_text_new (); + gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo_box), renderer, TRUE); + gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (combo_box), renderer, + "text", COL_LABEL, + NULL); + + profiles = mc_profiles_list (); + for (l = profiles; l; l = l->next) { + McProfile *profile; + + profile = l->data; + + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, + COL_ICON, gossip_pixbuf_from_profile (profile, GTK_ICON_SIZE_BUTTON), + COL_LABEL, mc_profile_get_display_name (profile), + COL_PROFILE, profile, + -1); + gtk_combo_box_set_active_iter (GTK_COMBO_BOX (combo_box), &iter); + } + + mc_profiles_free_list (profiles); + g_object_unref (store); + + return combo_box; +} + diff --git a/libempathy-gtk/gossip-profile-chooser.h b/libempathy-gtk/gossip-profile-chooser.h new file mode 100644 index 00000000..e608db7e --- /dev/null +++ b/libempathy-gtk/gossip-profile-chooser.h @@ -0,0 +1,34 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2006-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Xavier Claessens <xclaesse@gmail.com> + */ + +#ifndef __GOSSIP_PROTOCOL_CHOOSER_H__ +#define __GOSSIP_PROTOCOL_CHOOSER_H__ + +#include <libmissioncontrol/mc-profile.h> + +G_BEGIN_DECLS + +GtkWidget * gossip_profile_chooser_new (void); +McProfile * gossip_profile_chooser_get_selected (GtkWidget *widget); + +G_END_DECLS +#endif /* __GOSSIP_PROTOCOL_CHOOSER_H__ */ diff --git a/libempathy-gtk/gossip-spell.c b/libempathy-gtk/gossip-spell.c new file mode 100644 index 00000000..db06e9f1 --- /dev/null +++ b/libempathy-gtk/gossip-spell.c @@ -0,0 +1,457 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2004-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Martyn Russell <martyn@imendio.com> + * Richard Hult <richard@imendio.com> + */ + +#include "config.h" + +#include <string.h> +#include <stdlib.h> + +#include <glib/gi18n.h> + +#ifdef HAVE_ASPELL +#include <aspell.h> +#endif + +#include <libempathy/gossip-debug.h> +#include <libempathy/gossip-conf.h> + +#include "gossip-spell.h" +#include "gossip-preferences.h" + +#define DEBUG_DOMAIN "Spell" + +#ifdef HAVE_ASPELL + +/* Note: We could use aspell_reset_cache (NULL); periodically if we wanted + * to... + */ + +typedef struct { + AspellConfig *spell_config; + AspellCanHaveError *spell_possible_err; + AspellSpeller *spell_checker; +} SpellLanguage; + +#define ISO_CODES_DATADIR ISO_CODES_PREFIX "/share/xml/iso-codes" +#define ISO_CODES_LOCALESDIR ISO_CODES_PREFIX "/share/locale" + +static GHashTable *iso_code_names = NULL; +static GList *languages = NULL; +static gboolean gossip_conf_notify_inited = FALSE; + +static void +spell_iso_codes_parse_start_tag (GMarkupParseContext *ctx, + const gchar *element_name, + const gchar **attr_names, + const gchar **attr_values, + gpointer data, + GError **error) +{ + const gchar *ccode_longB, *ccode_longT, *ccode; + const gchar *lang_name; + + if (!g_str_equal (element_name, "iso_639_entry") || + attr_names == NULL || attr_values == NULL) { + return; + } + + ccode = NULL; + ccode_longB = NULL; + ccode_longT = NULL; + lang_name = NULL; + + while (*attr_names && *attr_values) { + if (g_str_equal (*attr_names, "iso_639_1_code")) { + if (**attr_values) { + ccode = *attr_values; + } + } + else if (g_str_equal (*attr_names, "iso_639_2B_code")) { + if (**attr_values) { + ccode_longB = *attr_values; + } + } + else if (g_str_equal (*attr_names, "iso_639_2T_code")) { + if (**attr_values) { + ccode_longT = *attr_values; + } + } + else if (g_str_equal (*attr_names, "name")) { + lang_name = *attr_values; + } + + attr_names++; + attr_values++; + } + + if (!lang_name) { + return; + } + + if (ccode) { + g_hash_table_insert (iso_code_names, + g_strdup (ccode), + g_strdup (lang_name)); + } + + if (ccode_longB) { + g_hash_table_insert (iso_code_names, + g_strdup (ccode_longB), + g_strdup (lang_name)); + } + + if (ccode_longT) { + g_hash_table_insert (iso_code_names, + g_strdup (ccode_longT), + g_strdup (lang_name)); + } +} + +static void +spell_iso_code_names_init (void) +{ + GError *err = NULL; + gchar *buf; + gsize buf_len; + + iso_code_names = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, g_free); + + bindtextdomain ("iso_639", ISO_CODES_LOCALESDIR); + bind_textdomain_codeset ("iso_639", "UTF-8"); + + /* FIXME: We should read this in chunks and pass to the parser. */ + if (g_file_get_contents (ISO_CODES_DATADIR "/iso_639.xml", &buf, &buf_len, &err)) { + GMarkupParseContext *ctx; + GMarkupParser parser = { + spell_iso_codes_parse_start_tag, + NULL, NULL, NULL, NULL + }; + + ctx = g_markup_parse_context_new (&parser, 0, NULL, NULL); + if (!g_markup_parse_context_parse (ctx, buf, buf_len, &err)) { + g_warning ("Failed to parse '%s': %s", + ISO_CODES_DATADIR"/iso_639.xml", + err->message); + g_error_free (err); + } + + g_markup_parse_context_free (ctx); + g_free (buf); + } else { + g_warning ("Failed to load '%s': %s", + ISO_CODES_DATADIR"/iso_639.xml", err->message); + g_error_free (err); + } +} + +static void +spell_notify_languages_cb (GossipConf *conf, + const gchar *key, + gpointer user_data) +{ + GList *l; + + gossip_debug (DEBUG_DOMAIN, "Resetting languages due to config change"); + + /* We just reset the languages list. */ + for (l = languages; l; l = l->next) { + SpellLanguage *lang; + + lang = l->data; + + delete_aspell_config (lang->spell_config); + delete_aspell_speller (lang->spell_checker); + + g_slice_free (SpellLanguage, lang); + } + + g_list_free (languages); + languages = NULL; +} + +static void +spell_setup_languages (void) +{ + gchar *str; + + if (!gossip_conf_notify_inited) { + gossip_conf_notify_add (gossip_conf_get (), + GOSSIP_PREFS_CHAT_SPELL_CHECKER_LANGUAGES, + spell_notify_languages_cb, NULL); + + gossip_conf_notify_inited = TRUE; + } + + if (languages) { + gossip_debug (DEBUG_DOMAIN, "No languages to setup"); + return; + } + + if (gossip_conf_get_string (gossip_conf_get (), + GOSSIP_PREFS_CHAT_SPELL_CHECKER_LANGUAGES, + &str) && str) { + gchar **strv; + gint i; + + strv = g_strsplit (str, ",", -1); + + i = 0; + while (strv && strv[i]) { + SpellLanguage *lang; + + gossip_debug (DEBUG_DOMAIN, "Setting up language:'%s'", strv[i]); + + lang = g_slice_new0 (SpellLanguage); + + lang->spell_config = new_aspell_config(); + + aspell_config_replace (lang->spell_config, "encoding", "utf-8"); + aspell_config_replace (lang->spell_config, "lang", strv[i++]); + + lang->spell_possible_err = new_aspell_speller (lang->spell_config); + + if (aspell_error_number (lang->spell_possible_err) == 0) { + lang->spell_checker = to_aspell_speller (lang->spell_possible_err); + languages = g_list_append (languages, lang); + } else { + delete_aspell_config (lang->spell_config); + g_slice_free (SpellLanguage, lang); + } + } + + if (strv) { + g_strfreev (strv); + } + + g_free (str); + } +} + +const char * +gossip_spell_get_language_name (const char *code) +{ + const gchar *name; + + g_return_val_if_fail (code != NULL, NULL); + + if (!iso_code_names) { + spell_iso_code_names_init (); + } + + name = g_hash_table_lookup (iso_code_names, code); + if (!name) { + return NULL; + } + + return dgettext ("iso_639", name); +} + +GList * +gossip_spell_get_language_codes (void) +{ + AspellConfig *config; + AspellDictInfoList *dlist; + AspellDictInfoEnumeration *dels; + const AspellDictInfo *entry; + GList *codes = NULL; + + config = new_aspell_config (); + dlist = get_aspell_dict_info_list (config); + dels = aspell_dict_info_list_elements (dlist); + + while ((entry = aspell_dict_info_enumeration_next (dels)) != 0) { + if (g_list_find_custom (codes, entry->code, (GCompareFunc) strcmp)) { + continue; + } + + codes = g_list_append (codes, g_strdup (entry->code)); + } + + delete_aspell_dict_info_enumeration (dels); + delete_aspell_config (config); + + return codes; +} + +void +gossip_spell_free_language_codes (GList *codes) +{ + g_list_foreach (codes, (GFunc) g_free, NULL); + g_list_free (codes); +} + +gboolean +gossip_spell_check (const gchar *word) +{ + GList *l; + gint n_langs; + gboolean correct = FALSE; + gint len; + const gchar *p; + gunichar c; + gboolean digit; + + g_return_val_if_fail (word != NULL, FALSE); + + spell_setup_languages (); + + if (!languages) { + gossip_debug (DEBUG_DOMAIN, "No languages to check against"); + return TRUE; + } + + /* Ignore certain cases like numbers, etc. */ + for (p = word, digit = TRUE; *p && digit; p = g_utf8_next_char (p)) { + c = g_utf8_get_char (p); + digit = g_unichar_isdigit (c); + } + + if (digit) { + /* We don't spell check digits. */ + gossip_debug (DEBUG_DOMAIN, "Not spell checking word:'%s', it is all digits", word); + return TRUE; + } + + len = strlen (word); + n_langs = g_list_length (languages); + for (l = languages; l; l = l->next) { + SpellLanguage *lang; + + lang = l->data; + + correct = aspell_speller_check (lang->spell_checker, word, len); + if (n_langs > 1 && correct) { + break; + } + } + + return correct; +} + +GList * +gossip_spell_get_suggestions (const gchar *word) +{ + GList *l1; + GList *l2 = NULL; + const AspellWordList *suggestions; + AspellStringEnumeration *elements; + const char *next; + gint len; + + g_return_val_if_fail (word != NULL, NULL); + + spell_setup_languages (); + + len = strlen (word); + + for (l1 = languages; l1; l1 = l1->next) { + SpellLanguage *lang; + + lang = l1->data; + + suggestions = aspell_speller_suggest (lang->spell_checker, + word, len); + + elements = aspell_word_list_elements (suggestions); + + while ((next = aspell_string_enumeration_next (elements))) { + l2 = g_list_append (l2, g_strdup (next)); + } + + delete_aspell_string_enumeration (elements); + } + + return l2; +} + +gboolean +gossip_spell_supported (void) +{ + if (g_getenv ("GOSSIP_SPELL_DISABLED")) { + gossip_debug (DEBUG_DOMAIN, "GOSSIP_SPELL_DISABLE env variable defined"); + return FALSE; + } + + gossip_debug (DEBUG_DOMAIN, "Support enabled"); + + return TRUE; +} + +#else /* not HAVE_ASPELL */ + +gboolean +gossip_spell_supported (void) +{ + gossip_debug (DEBUG_DOMAIN, "Support disabled"); + + return FALSE; +} + +GList * +gossip_spell_get_suggestions (const gchar *word) +{ + gossip_debug (DEBUG_DOMAIN, "Support disabled, could not get suggestions"); + + return NULL; +} + +gboolean +gossip_spell_check (const gchar *word) +{ + gossip_debug (DEBUG_DOMAIN, "Support disabled, could not check spelling"); + + return TRUE; +} + +const char * +gossip_spell_get_language_name (const char *lang) +{ + gossip_debug (DEBUG_DOMAIN, "Support disabled, could not get language name"); + + return NULL; +} + +GList * +gossip_spell_get_language_codes (void) +{ + gossip_debug (DEBUG_DOMAIN, "Support disabled, could not get language codes"); + + return NULL; +} + +void +gossip_spell_free_language_codes (GList *codes) +{ +} + +#endif /* HAVE_ASPELL */ + + +void +gossip_spell_free_suggestions (GList *suggestions) +{ + g_list_foreach (suggestions, (GFunc) g_free, NULL); + g_list_free (suggestions); +} + diff --git a/libempathy-gtk/gossip-spell.h b/libempathy-gtk/gossip-spell.h new file mode 100644 index 00000000..f2d841b3 --- /dev/null +++ b/libempathy-gtk/gossip-spell.h @@ -0,0 +1,39 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2004-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Martyn Russell <martyn@imendio.com> + * Richard Hult <richard@imendio.com> + */ + +#ifndef __GOSSIP_SPELL_H__ +#define __GOSSIP_SPELL_H__ + +G_BEGIN_DECLS + +gboolean gossip_spell_supported (void); +const gchar *gossip_spell_get_language_name (const gchar *code); +GList *gossip_spell_get_language_codes (void); +void gossip_spell_free_language_codes (GList *codes); +gboolean gossip_spell_check (const gchar *word); +GList * gossip_spell_get_suggestions (const gchar *word); +void gossip_spell_free_suggestions (GList *suggestions); + +G_END_DECLS + +#endif /* __GOSSIP_SPELL_H__ */ diff --git a/libempathy-gtk/gossip-stock.c b/libempathy-gtk/gossip-stock.c new file mode 100644 index 00000000..f43949ee --- /dev/null +++ b/libempathy-gtk/gossip-stock.c @@ -0,0 +1,105 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2003-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Mikael Hallendal <micke@imendio.com> + * Richard Hult <richard@imendio.com> + * Martyn Russell <martyn@imendio.com> + */ + +#include "config.h" + +#include <gtk/gtk.h> + +#include <libempathy/gossip-paths.h> + +#include "gossip-stock.h" + +static GtkIconFactory *icon_factory = NULL; +static GtkWidget *main_widget = NULL; + +static GtkStockItem stock_items[] = { + { GOSSIP_STOCK_OFFLINE, NULL }, + { GOSSIP_STOCK_AVAILABLE, NULL }, + { GOSSIP_STOCK_BUSY, NULL }, + { GOSSIP_STOCK_AWAY, NULL }, + { GOSSIP_STOCK_EXT_AWAY, NULL }, + { GOSSIP_STOCK_PENDING, NULL }, + { GOSSIP_STOCK_MESSAGE, NULL }, + { GOSSIP_STOCK_TYPING, NULL }, + { GOSSIP_STOCK_CONTACT_INFORMATION, NULL }, + { GOSSIP_STOCK_GROUP_MESSAGE, NULL } +}; + +void +gossip_stock_init (GtkWidget *widget) +{ + GtkIconSet *icon_set; + gint i; + + g_assert (icon_factory == NULL); + + main_widget = g_object_ref (widget); + + gtk_stock_add (stock_items, G_N_ELEMENTS (stock_items)); + + icon_factory = gtk_icon_factory_new (); + gtk_icon_factory_add_default (icon_factory); + g_object_unref (icon_factory); + + for (i = 0; i < G_N_ELEMENTS (stock_items); i++) { + gchar *path, *filename; + GdkPixbuf *pixbuf; + + filename = g_strdup_printf ("%s.png", stock_items[i].stock_id); + path = gossip_paths_get_image_path (filename); + pixbuf = gdk_pixbuf_new_from_file (path, NULL); + g_free (path); + g_free (filename); + + icon_set = gtk_icon_set_new_from_pixbuf (pixbuf); + + gtk_icon_factory_add (icon_factory, + stock_items[i].stock_id, + icon_set); + + gtk_icon_set_unref (icon_set); + + g_object_unref (pixbuf); + } +} + +void +gossip_stock_finalize (void) +{ + g_assert (icon_factory != NULL); + + gtk_icon_factory_remove_default (icon_factory); + g_object_unref (main_widget); + + main_widget = NULL; + icon_factory = NULL; +} + +GdkPixbuf * +gossip_stock_render (const gchar *stock, + GtkIconSize size) +{ + return gtk_widget_render_icon (main_widget, stock, size, NULL); +} + diff --git a/libempathy-gtk/gossip-stock.h b/libempathy-gtk/gossip-stock.h new file mode 100644 index 00000000..412aceef --- /dev/null +++ b/libempathy-gtk/gossip-stock.h @@ -0,0 +1,60 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2003-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Mikael Hallendal <micke@imendio.com> + * Richard Hult <richard@imendio.com> + * Martyn Russell <martyn@imendio.com> + */ + +#ifndef __GOSSIP_STOCK_H__ +#define __GOSSIP_STOCK_H__ + +#include <glib.h> +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define GOSSIP_STOCK_OFFLINE "gossip-offline" +#define GOSSIP_STOCK_AVAILABLE "gossip-available" +#define GOSSIP_STOCK_BUSY "gossip-busy" +#define GOSSIP_STOCK_AWAY "gossip-away" +#define GOSSIP_STOCK_EXT_AWAY "gossip-extended-away" +#define GOSSIP_STOCK_PENDING "gossip-pending" + +#define GOSSIP_STOCK_MESSAGE "gossip-message" +#define GOSSIP_STOCK_TYPING "gossip-typing" + + +#define GOSSIP_STOCK_CONTACT_INFORMATION "vcard_16" + +#define GOSSIP_STOCK_AIM "gossip-aim" +#define GOSSIP_STOCK_ICQ "gossip-icq" +#define GOSSIP_STOCK_MSN "gossip-msn" +#define GOSSIP_STOCK_YAHOO "gossip-yahoo" + +#define GOSSIP_STOCK_GROUP_MESSAGE "gossip-group-message" + +void gossip_stock_init (GtkWidget *widget); +void gossip_stock_finalize (void); +GdkPixbuf * gossip_stock_render (const gchar *stock, + GtkIconSize size); + +G_END_DECLS + +#endif /* __GOSSIP_STOCK_ICONS_H__ */ diff --git a/libempathy-gtk/gossip-theme-manager.c b/libempathy-gtk/gossip-theme-manager.c new file mode 100644 index 00000000..6d5905e5 --- /dev/null +++ b/libempathy-gtk/gossip-theme-manager.c @@ -0,0 +1,1045 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2005-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Richard Hult <richard@imendio.com> + */ + +#include "config.h" + +#include <string.h> + +#include <glib/gi18n.h> +#include <gtk/gtk.h> + +#include <libempathy/gossip-conf.h> +#include <libempathy/gossip-utils.h> + +#include "gossip-chat-view.h" +#include "gossip-preferences.h" +#include "gossip-theme-manager.h" + +#define GET_PRIV(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GOSSIP_TYPE_THEME_MANAGER, GossipThemeManagerPriv)) + +typedef struct { + gchar *name; + guint name_notify_id; + guint room_notify_id; + + gboolean show_avatars; + guint show_avatars_notify_id; + + gboolean irc_style; +} GossipThemeManagerPriv; + +static void theme_manager_finalize (GObject *object); +static void theme_manager_notify_name_cb (GossipConf *conf, + const gchar *key, + gpointer user_data); +static void theme_manager_notify_room_cb (GossipConf *conf, + const gchar *key, + gpointer user_data); +static void theme_manager_notify_show_avatars_cb (GossipConf *conf, + const gchar *key, + gpointer user_data); +static void theme_manager_ensure_tag_by_name (GtkTextBuffer *buffer, + const gchar *name); +static gboolean theme_manager_ensure_theme_exists (const gchar *name); +static GtkTextTag *theme_manager_init_tag_by_name (GtkTextTagTable *table, + const gchar *name); +static void theme_manager_add_tag (GtkTextTagTable *table, + GtkTextTag *tag); +static void theme_manager_fixup_tag_table (GossipThemeManager *theme_manager, + GossipChatView *view); +static void theme_manager_apply_theme_classic (GossipThemeManager *manager, + GossipChatView *view); +static void theme_manager_apply_theme_clean (GossipThemeManager *manager, + GossipChatView *view); +static void theme_manager_apply_theme_blue (GossipThemeManager *manager, + GossipChatView *view); +static void theme_manager_apply_theme (GossipThemeManager *manager, + GossipChatView *view, + const gchar *name); + +enum { + THEME_CHANGED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0 }; + +static const gchar *themes[] = { + "classic", N_("Classic"), + "simple", N_("Simple"), + "clean", N_("Clean"), + "blue", N_("Blue"), + NULL +}; + +G_DEFINE_TYPE (GossipThemeManager, gossip_theme_manager, G_TYPE_OBJECT); + +static void +gossip_theme_manager_class_init (GossipThemeManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + signals[THEME_CHANGED] = + g_signal_new ("theme-changed", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); + + g_type_class_add_private (object_class, sizeof (GossipThemeManagerPriv)); + + object_class->finalize = theme_manager_finalize; +} + +static void +gossip_theme_manager_init (GossipThemeManager *manager) +{ + GossipThemeManagerPriv *priv; + + priv = GET_PRIV (manager); + + priv->name_notify_id = + gossip_conf_notify_add (gossip_conf_get (), + GOSSIP_PREFS_CHAT_THEME, + theme_manager_notify_name_cb, + manager); + + priv->room_notify_id = + gossip_conf_notify_add (gossip_conf_get (), + GOSSIP_PREFS_CHAT_THEME_CHAT_ROOM, + theme_manager_notify_room_cb, + manager); + + gossip_conf_get_string (gossip_conf_get (), + GOSSIP_PREFS_CHAT_THEME, + &priv->name); + + /* Unused right now, but will be used soon. */ + priv->show_avatars_notify_id = + gossip_conf_notify_add (gossip_conf_get (), + GOSSIP_PREFS_UI_SHOW_AVATARS, + theme_manager_notify_show_avatars_cb, + manager); + + gossip_conf_get_bool (gossip_conf_get (), + GOSSIP_PREFS_UI_SHOW_AVATARS, + &priv->show_avatars); +} + +static void +theme_manager_finalize (GObject *object) +{ + GossipThemeManagerPriv *priv; + + priv = GET_PRIV (object); + + gossip_conf_notify_remove (gossip_conf_get (), priv->name_notify_id); + gossip_conf_notify_remove (gossip_conf_get (), priv->room_notify_id); + gossip_conf_notify_remove (gossip_conf_get (), priv->show_avatars_notify_id); + + g_free (priv->name); + + G_OBJECT_CLASS (gossip_theme_manager_parent_class)->finalize (object); +} + +static void +theme_manager_notify_name_cb (GossipConf *conf, + const gchar *key, + gpointer user_data) +{ + GossipThemeManager *manager; + GossipThemeManagerPriv *priv; + gchar *name; + + manager = user_data; + priv = GET_PRIV (manager); + + g_free (priv->name); + + name = NULL; + if (!gossip_conf_get_string (conf, key, &name) || + name == NULL || name[0] == 0) { + priv->name = g_strdup ("classic"); + g_free (name); + } else { + priv->name = name; + } + + g_signal_emit (manager, signals[THEME_CHANGED], 0, NULL); +} + +static void +theme_manager_notify_room_cb (GossipConf *conf, + const gchar *key, + gpointer user_data) +{ + g_signal_emit (user_data, signals[THEME_CHANGED], 0, NULL); +} + +static void +theme_manager_notify_show_avatars_cb (GossipConf *conf, + const gchar *key, + gpointer user_data) +{ + GossipThemeManager *manager; + GossipThemeManagerPriv *priv; + gboolean value; + + manager = user_data; + priv = GET_PRIV (manager); + + if (!gossip_conf_get_bool (conf, key, &value)) { + priv->show_avatars = FALSE; + } else { + priv->show_avatars = value; + } +} + +static void +theme_manager_ensure_tag_by_name (GtkTextBuffer *buffer, + const gchar *name) +{ + GtkTextTagTable *table; + GtkTextTag *tag; + + table = gtk_text_buffer_get_tag_table (buffer); + tag = gtk_text_tag_table_lookup (table, name); + + if (!tag) { + gtk_text_buffer_create_tag (buffer, + name, + NULL); + } +} + +static gboolean +theme_manager_ensure_theme_exists (const gchar *name) +{ + gint i; + + if (G_STR_EMPTY (name)) { + return FALSE; + } + + for (i = 0; themes[i]; i += 2) { + if (strcmp (themes[i], name) == 0) { + return TRUE; + } + } + + return FALSE; +} + +static GtkTextTag * +theme_manager_init_tag_by_name (GtkTextTagTable *table, + const gchar *name) +{ + GtkTextTag *tag; + + tag = gtk_text_tag_table_lookup (table, name); + + if (!tag) { + return gtk_text_tag_new (name); + } + + /* Clear the old values so that we don't affect the new theme. */ + g_object_set (tag, + "background-set", FALSE, + "foreground-set", FALSE, + "invisible-set", FALSE, + "justification-set", FALSE, + "paragraph-background-set", FALSE, + "pixels-above-lines-set", FALSE, + "pixels-below-lines-set", FALSE, + "rise-set", FALSE, + "scale-set", FALSE, + "size-set", FALSE, + "style-set", FALSE, + "weight-set", FALSE, + NULL); + + return tag; +} + +static void +theme_manager_add_tag (GtkTextTagTable *table, + GtkTextTag *tag) +{ + gchar *name; + GtkTextTag *check_tag; + + g_object_get (tag, "name", &name, NULL); + check_tag = gtk_text_tag_table_lookup (table, name); + g_free (name); + if (check_tag) { + return; + } + + gtk_text_tag_table_add (table, tag); + + g_object_unref (tag); +} + +static void +theme_manager_fixup_tag_table (GossipThemeManager *theme_manager, + GossipChatView *view) +{ + GtkTextBuffer *buffer; + + buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)); + + /* "Fancy" style tags. */ + theme_manager_ensure_tag_by_name (buffer, "fancy-header-self"); + theme_manager_ensure_tag_by_name (buffer, "fancy-header-self-avatar"); + theme_manager_ensure_tag_by_name (buffer, "fancy-avatar-self"); + theme_manager_ensure_tag_by_name (buffer, "fancy-line-top-self"); + theme_manager_ensure_tag_by_name (buffer, "fancy-line-bottom-self"); + theme_manager_ensure_tag_by_name (buffer, "fancy-body-self"); + theme_manager_ensure_tag_by_name (buffer, "fancy-action-self"); + theme_manager_ensure_tag_by_name (buffer, "fancy-highlight-self"); + + theme_manager_ensure_tag_by_name (buffer, "fancy-header-other"); + theme_manager_ensure_tag_by_name (buffer, "fancy-header-other-avatar"); + theme_manager_ensure_tag_by_name (buffer, "fancy-avatar-other"); + theme_manager_ensure_tag_by_name (buffer, "fancy-line-top-other"); + theme_manager_ensure_tag_by_name (buffer, "fancy-line-bottom-other"); + theme_manager_ensure_tag_by_name (buffer, "fancy-body-other"); + theme_manager_ensure_tag_by_name (buffer, "fancy-action-other"); + theme_manager_ensure_tag_by_name (buffer, "fancy-highlight-other"); + + theme_manager_ensure_tag_by_name (buffer, "fancy-spacing"); + theme_manager_ensure_tag_by_name (buffer, "fancy-time"); + theme_manager_ensure_tag_by_name (buffer, "fancy-event"); + theme_manager_ensure_tag_by_name (buffer, "fancy-invite"); + theme_manager_ensure_tag_by_name (buffer, "fancy-link"); + + /* IRC style tags. */ + theme_manager_ensure_tag_by_name (buffer, "irc-nick-self"); + theme_manager_ensure_tag_by_name (buffer, "irc-body-self"); + theme_manager_ensure_tag_by_name (buffer, "irc-action-self"); + + theme_manager_ensure_tag_by_name (buffer, "irc-nick-other"); + theme_manager_ensure_tag_by_name (buffer, "irc-body-other"); + theme_manager_ensure_tag_by_name (buffer, "irc-action-other"); + + theme_manager_ensure_tag_by_name (buffer, "irc-nick-highlight"); + theme_manager_ensure_tag_by_name (buffer, "irc-spacing"); + theme_manager_ensure_tag_by_name (buffer, "irc-time"); + theme_manager_ensure_tag_by_name (buffer, "irc-event"); + theme_manager_ensure_tag_by_name (buffer, "irc-invite"); + theme_manager_ensure_tag_by_name (buffer, "irc-link"); +} + +static void +theme_manager_apply_theme_classic (GossipThemeManager *manager, + GossipChatView *view) +{ + GossipThemeManagerPriv *priv; + GtkTextBuffer *buffer; + GtkTextTagTable *table; + GtkTextTag *tag; + + priv = GET_PRIV (manager); + + buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)); + table = gtk_text_buffer_get_tag_table (buffer); + + priv->irc_style = TRUE; + + tag = theme_manager_init_tag_by_name (table, "irc-spacing"); + g_object_set (tag, + "size", 2000, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "irc-nick-self"); + g_object_set (tag, + "foreground", "sea green", + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "irc-body-self"); + g_object_set (tag, + /* To get the default theme color: */ + "foreground-set", FALSE, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "irc-action-self"); + g_object_set (tag, + "foreground", "brown4", + "style", PANGO_STYLE_ITALIC, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "irc-nick-highlight"); + g_object_set (tag, + "foreground", "indian red", + "weight", PANGO_WEIGHT_BOLD, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "irc-nick-other"); + g_object_set (tag, + "foreground", "skyblue4", + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "irc-body-other"); + g_object_set (tag, + /* To get the default theme color: */ + "foreground-set", FALSE, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "irc-action-other"); + g_object_set (tag, + "foreground", "brown4", + "style", PANGO_STYLE_ITALIC, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "irc-time"); + g_object_set (tag, + "foreground", "darkgrey", + "justification", GTK_JUSTIFY_CENTER, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "irc-event"); + g_object_set (tag, + "foreground", "PeachPuff4", + "justification", GTK_JUSTIFY_LEFT, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "irc-invite"); + g_object_set (tag, + "foreground", "sienna", + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "irc-link"); + g_object_set (tag, + "foreground", "steelblue", + "underline", PANGO_UNDERLINE_SINGLE, + NULL); + theme_manager_add_tag (table, tag); +} + +static void +theme_manager_apply_theme_simple (GossipThemeManager *manager, + GossipChatView *view) +{ + GossipThemeManagerPriv *priv; + GtkTextBuffer *buffer; + GtkTextTagTable *table; + GtkTextTag *tag; + GtkWidget *widget; + GtkStyle *style; + + priv = GET_PRIV (manager); + + widget = gtk_entry_new (); + style = gtk_widget_get_style (widget); + gtk_widget_destroy (widget); + + buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)); + table = gtk_text_buffer_get_tag_table (buffer); + + priv->irc_style = FALSE; + + tag = theme_manager_init_tag_by_name (table, "fancy-spacing"); + g_object_set (tag, + "size", 3000, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-header-self"); + g_object_set (tag, + "weight", PANGO_WEIGHT_BOLD, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-header-self-avatar"); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-avatar-self"); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-line-top-self"); + g_object_set (tag, + "size", 6 * PANGO_SCALE, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-line-bottom-self"); + g_object_set (tag, + "size", 1, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-body-self"); + g_object_set (tag, + "pixels-above-lines", 2, + "pixels-below-lines", 2, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-action-self"); + g_object_set (tag, + "foreground-gdk", &style->base[GTK_STATE_SELECTED], + "style", PANGO_STYLE_ITALIC, + "pixels-above-lines", 2, + "pixels-below-lines", 2, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-highlight-self"); + g_object_set (tag, + "weight", PANGO_WEIGHT_BOLD, + "pixels-above-lines", 2, + "pixels-below-lines", 2, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-header-other"); + g_object_set (tag, + "weight", PANGO_WEIGHT_BOLD, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-header-other-avatar"); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-avatar-other"); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-line-top-other"); + g_object_set (tag, + "size", 6 * PANGO_SCALE, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-line-bottom-other"); + g_object_set (tag, + "size", 1, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-body-other"); + g_object_set (tag, + "pixels-above-lines", 2, + "pixels-below-lines", 2, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-action-other"); + g_object_set (tag, + "foreground-gdk", &style->base[GTK_STATE_SELECTED], + "style", PANGO_STYLE_ITALIC, + "pixels-above-lines", 2, + "pixels-below-lines", 2, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-highlight-other"); + g_object_set (tag, + "weight", PANGO_WEIGHT_BOLD, + "pixels-above-lines", 2, + "pixels-below-lines", 2, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-time"); + g_object_set (tag, + "foreground", "darkgrey", + "justification", GTK_JUSTIFY_CENTER, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-event"); + g_object_set (tag, + "foreground", "darkgrey", + "justification", GTK_JUSTIFY_LEFT, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-invite"); + g_object_set (tag, + "foreground", "darkgrey", + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-link"); + g_object_set (tag, + "foreground-gdk", &style->base[GTK_STATE_SELECTED], + "underline", PANGO_UNDERLINE_SINGLE, + NULL); + theme_manager_add_tag (table, tag); +} + +static void +theme_manager_apply_theme_clean (GossipThemeManager *manager, + GossipChatView *view) +{ + GossipThemeManagerPriv *priv; + GtkTextBuffer *buffer; + GtkTextTagTable *table; + GtkTextTag *tag; + + priv = GET_PRIV (manager); + + /* Inherit the simple theme. */ + theme_manager_apply_theme_simple (manager, view); + +#define ELEGANT_HEAD "#efefdf" +#define ELEGANT_LINE "#e3e3d3" + + buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)); + table = gtk_text_buffer_get_tag_table (buffer); + + tag = theme_manager_init_tag_by_name (table, "fancy-spacing"); + g_object_set (tag, + "size", PANGO_SCALE * 10, + NULL); + + tag = theme_manager_init_tag_by_name (table, "fancy-header-self"); + g_object_set (tag, + "foreground", "black", + "weight", PANGO_WEIGHT_BOLD, + "paragraph-background", ELEGANT_HEAD, + "pixels-above-lines", 2, + "pixels-below-lines", 2, + NULL); + + tag = theme_manager_init_tag_by_name (table, "fancy-avatar-self"); + g_object_set (tag, + "paragraph-background", ELEGANT_HEAD, + "pixels-above-lines", 2, + "pixels-below-lines", 2, + NULL); + + tag = theme_manager_init_tag_by_name (table, "fancy-line-top-self"); + g_object_set (tag, + "size", 1 * PANGO_SCALE, + "paragraph-background", ELEGANT_LINE, + NULL); + + tag = theme_manager_init_tag_by_name (table, "fancy-line-bottom-self"); + g_object_set (tag, + "size", 1 * PANGO_SCALE, + NULL); + + tag = theme_manager_init_tag_by_name (table, "fancy-action-self"); + g_object_set (tag, + "foreground", "brown4", + "style", PANGO_STYLE_ITALIC, + NULL); + + tag = theme_manager_init_tag_by_name (table, "fancy-highlight-self"); + g_object_set (tag, + "foreground", "black", + "weight", PANGO_WEIGHT_BOLD, + NULL); + + tag = theme_manager_init_tag_by_name (table, "fancy-header-other"); + g_object_set (tag, + "foreground", "black", + "weight", PANGO_WEIGHT_BOLD, + "paragraph-background", ELEGANT_HEAD, + "pixels-above-lines", 2, + "pixels-below-lines", 2, + NULL); + + tag = theme_manager_init_tag_by_name (table, "fancy-avatar-other"); + g_object_set (tag, + "paragraph-background", ELEGANT_HEAD, + "pixels-above-lines", 2, + "pixels-below-lines", 2, + NULL); + + tag = theme_manager_init_tag_by_name (table, "fancy-line-top-other"); + g_object_set (tag, + "size", 1 * PANGO_SCALE, + "paragraph-background", ELEGANT_LINE, + NULL); + + tag = theme_manager_init_tag_by_name (table, "fancy-line-bottom-other"); + g_object_set (tag, + "size", 1 * PANGO_SCALE, + NULL); + + tag = theme_manager_init_tag_by_name (table, "fancy-action-other"); + g_object_set (tag, + "foreground", "brown4", + "style", PANGO_STYLE_ITALIC, + NULL); + + tag = theme_manager_init_tag_by_name (table, "fancy-time"); + g_object_set (tag, + "foreground", "darkgrey", + "justification", GTK_JUSTIFY_CENTER, + NULL); + + tag = theme_manager_init_tag_by_name (table, "fancy-event"); + g_object_set (tag, + "foreground", "darkgrey", + "justification", GTK_JUSTIFY_LEFT, + NULL); + + tag = theme_manager_init_tag_by_name (table, "fancy-invite"); + g_object_set (tag, + "foreground", "sienna", + NULL); + + tag = theme_manager_init_tag_by_name (table, "fancy-link"); + g_object_set (tag, + "foreground", "#49789e", + "underline", PANGO_UNDERLINE_SINGLE, + NULL); +} + +static void +theme_manager_apply_theme_blue (GossipThemeManager *manager, + GossipChatView *view) +{ + GossipThemeManagerPriv *priv; + GtkTextBuffer *buffer; + GtkTextTagTable *table; + GtkTextTag *tag; + + priv = GET_PRIV (manager); + + buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)); + table = gtk_text_buffer_get_tag_table (buffer); + + priv->irc_style = FALSE; + +#define BLUE_BODY_SELF "#dcdcdc" +#define BLUE_HEAD_SELF "#b9b9b9" +#define BLUE_LINE_SELF "#aeaeae" + +#define BLUE_BODY_OTHER "#adbdc8" +#define BLUE_HEAD_OTHER "#88a2b4" +#define BLUE_LINE_OTHER "#7f96a4" + + tag = theme_manager_init_tag_by_name (table, "fancy-spacing"); + g_object_set (tag, + "size", 3000, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-header-self"); + g_object_set (tag, + "foreground", "black", + "paragraph-background", BLUE_HEAD_SELF, + "weight", PANGO_WEIGHT_BOLD, + "pixels-above-lines", 2, + "pixels-below-lines", 2, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-header-self-avatar"); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-avatar-self"); + g_object_set (tag, + "paragraph-background", BLUE_HEAD_SELF, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-line-top-self"); + g_object_set (tag, + "size", 1, + "paragraph-background", BLUE_LINE_SELF, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-line-bottom-self"); + g_object_set (tag, + "size", 1, + "paragraph-background", BLUE_LINE_SELF, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-body-self"); + g_object_set (tag, + "foreground", "black", + "paragraph-background", BLUE_BODY_SELF, + "pixels-above-lines", 4, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-action-self"); + g_object_set (tag, + "foreground", "brown4", + "style", PANGO_STYLE_ITALIC, + "paragraph-background", BLUE_BODY_SELF, + "pixels-above-lines", 4, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-highlight-self"); + g_object_set (tag, + "foreground", "black", + "weight", PANGO_WEIGHT_BOLD, + "paragraph-background", BLUE_BODY_SELF, + "pixels-above-lines", 4, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-header-other"); + g_object_set (tag, + "foreground", "black", + "paragraph-background", BLUE_HEAD_OTHER, + "weight", PANGO_WEIGHT_BOLD, + "pixels-above-lines", 2, + "pixels-below-lines", 2, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-header-other-avatar"); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-avatar-other"); + g_object_set (tag, + "paragraph-background", BLUE_HEAD_OTHER, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-line-top-other"); + g_object_set (tag, + "size", 1, + "paragraph-background", BLUE_LINE_OTHER, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-line-bottom-other"); + g_object_set (tag, + "size", 1, + "paragraph-background", BLUE_LINE_OTHER, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-body-other"); + g_object_set (tag, + "foreground", "black", + "paragraph-background", BLUE_BODY_OTHER, + "pixels-above-lines", 4, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-action-other"); + g_object_set (tag, + "foreground", "brown4", + "style", PANGO_STYLE_ITALIC, + "paragraph-background", BLUE_BODY_OTHER, + "pixels-above-lines", 4, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-highlight-other"); + g_object_set (tag, + "foreground", "black", + "weight", PANGO_WEIGHT_BOLD, + "paragraph-background", BLUE_BODY_OTHER, + "pixels-above-lines", 4, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-time"); + g_object_set (tag, + "foreground", "darkgrey", + "justification", GTK_JUSTIFY_CENTER, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-event"); + g_object_set (tag, + "foreground", BLUE_LINE_OTHER, + "justification", GTK_JUSTIFY_LEFT, + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-invite"); + g_object_set (tag, + "foreground", "sienna", + NULL); + theme_manager_add_tag (table, tag); + + tag = theme_manager_init_tag_by_name (table, "fancy-link"); + g_object_set (tag, + "foreground", "#49789e", + "underline", PANGO_UNDERLINE_SINGLE, + NULL); + theme_manager_add_tag (table, tag); +} + +static void +theme_manager_apply_theme (GossipThemeManager *manager, + GossipChatView *view, + const gchar *name) +{ + GossipThemeManagerPriv *priv; + gint margin; + + priv = GET_PRIV (manager); + + /* Make sure all tags are present. Note: not useful now but when we have + * user defined theme it will be. + */ + theme_manager_fixup_tag_table (manager, view); + + if (theme_manager_ensure_theme_exists (name)) { + if (strcmp (name, "clean") == 0) { + theme_manager_apply_theme_clean (manager, view); + margin = 3; + } + else if (strcmp (name, "simple") == 0) { + theme_manager_apply_theme_simple (manager, view); + margin = 3; + } + else if (strcmp (name, "blue") == 0) { + theme_manager_apply_theme_blue (manager, view); + margin = 0; + } else { + theme_manager_apply_theme_classic (manager, view); + margin = 3; + } + } else { + theme_manager_apply_theme_classic (manager, view); + margin = 3; + } + + gossip_chat_view_set_margin (view, margin); + gossip_chat_view_set_irc_style (view, priv->irc_style); +} + +GossipThemeManager * +gossip_theme_manager_get (void) +{ + static GossipThemeManager *manager = NULL; + + if (!manager) { + manager = g_object_new (GOSSIP_TYPE_THEME_MANAGER, NULL); + } + + return manager; +} + +const gchar ** +gossip_theme_manager_get_themes (void) +{ + return themes; +} + +void +gossip_theme_manager_apply (GossipThemeManager *manager, + GossipChatView *view, + const gchar *name) +{ + GossipThemeManagerPriv *priv; + + priv = GET_PRIV (manager); + + theme_manager_apply_theme (manager, view, name); +} + +void +gossip_theme_manager_apply_saved (GossipThemeManager *manager, + GossipChatView *view) +{ + GossipThemeManagerPriv *priv; + + priv = GET_PRIV (manager); + + theme_manager_apply_theme (manager, view, priv->name); +} + +/* FIXME: A bit ugly. We should probably change the scheme so that instead of + * the manager signalling, views are registered and applied to automatically. + */ +void +gossip_theme_manager_update_show_avatars (GossipThemeManager *manager, + GossipChatView *view, + gboolean show) +{ + GossipThemeManagerPriv *priv; + GtkTextBuffer *buffer; + GtkTextTagTable *table; + GtkTextTag *tag_text_self, *tag_text_other; + GtkTextTag *tag_image_self, *tag_image_other; + + priv = GET_PRIV (manager); + + buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)); + table = gtk_text_buffer_get_tag_table (buffer); + + tag_text_self = gtk_text_tag_table_lookup (table, "fancy-header-self-avatar"); + tag_text_other = gtk_text_tag_table_lookup (table, "fancy-header-other-avatar"); + + tag_image_self = gtk_text_tag_table_lookup (table, "fancy-avatar-self"); + tag_image_other = gtk_text_tag_table_lookup (table, "fancy-avatar-other"); + + if (!show) { + g_object_set (tag_text_self, + "rise", 0, + NULL); + g_object_set (tag_text_other, + "rise", 0, + NULL); + g_object_set (tag_image_self, + "invisible", TRUE, + NULL); + g_object_set (tag_image_other, + "invisible", TRUE, + NULL); + } else { + GtkTextAttributes *attrs; + gint size; + gint rise; + + attrs = gtk_text_view_get_default_attributes (GTK_TEXT_VIEW (view)); + size = pango_font_description_get_size (attrs->font); + rise = MAX (0, (32 * PANGO_SCALE - size) / 2.0); + + g_object_set (tag_text_self, + "rise", rise, + NULL); + g_object_set (tag_text_other, + "rise", rise, + NULL); + g_object_set (tag_image_self, + "invisible", FALSE, + NULL); + g_object_set (tag_image_other, + "invisible", FALSE, + NULL); + + gtk_text_attributes_unref (attrs); + } +} + diff --git a/libempathy-gtk/gossip-theme-manager.h b/libempathy-gtk/gossip-theme-manager.h new file mode 100644 index 00000000..bb7e3cf7 --- /dev/null +++ b/libempathy-gtk/gossip-theme-manager.h @@ -0,0 +1,64 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2005-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Richard Hult <richard@imendio.com> + */ + +#ifndef __GOSSIP_THEME_MANAGER_H__ +#define __GOSSIP_THEME_MANAGER_H__ + +#include <glib-object.h> + +#include "gossip-chat-view.h" + +G_BEGIN_DECLS + +#define GOSSIP_TYPE_THEME_MANAGER (gossip_theme_manager_get_type ()) +#define GOSSIP_THEME_MANAGER(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GOSSIP_TYPE_THEME_MANAGER, GossipThemeManager)) +#define GOSSIP_THEME_MANAGER_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GOSSIP_TYPE_THEME_MANAGER, GossipThemeManagerClass)) +#define GOSSIP_IS_THEME_MANAGER(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GOSSIP_TYPE_THEME_MANAGER)) +#define GOSSIP_IS_THEME_MANAGER_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GOSSIP_TYPE_THEME_MANAGER)) +#define GOSSIP_THEME_MANAGER_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GOSSIP_TYPE_THEME_MANAGER, GossipThemeManagerClass)) + +typedef struct _GossipThemeManager GossipThemeManager; +typedef struct _GossipThemeManagerClass GossipThemeManagerClass; + +struct _GossipThemeManager { + GObject parent; +}; + +struct _GossipThemeManagerClass { + GObjectClass parent_class; +}; + +GType gossip_theme_manager_get_type (void) G_GNUC_CONST; +GossipThemeManager *gossip_theme_manager_get (void); +const gchar ** gossip_theme_manager_get_themes (void); +void gossip_theme_manager_apply (GossipThemeManager *manager, + GossipChatView *view, + const gchar *theme); +void gossip_theme_manager_apply_saved (GossipThemeManager *manager, + GossipChatView *view); +void gossip_theme_manager_update_show_avatars (GossipThemeManager *manager, + GossipChatView *view, + gboolean show); + +G_END_DECLS + +#endif /* __GOSSIP_THEME_MANAGER_H__ */ diff --git a/libempathy-gtk/gossip-ui-utils.c b/libempathy-gtk/gossip-ui-utils.c new file mode 100644 index 00000000..e902012f --- /dev/null +++ b/libempathy-gtk/gossip-ui-utils.c @@ -0,0 +1,1360 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2002-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Mikael Hallendal <micke@imendio.com> + * Richard Hult <richard@imendio.com> + * Martyn Russell <martyn@imendio.com> + * + * Part of this file is copied from GtkSourceView (gtksourceiter.c): + * Paolo Maggi + * Jeroen Zwartepoorte + */ + +#include <string.h> + +#include <glib/gi18n.h> +#include <gtk/gtk.h> +#include <glade/glade.h> +#include <libgnome/libgnome.h> + +#include <libmissioncontrol/mc-profile.h> + +#include <libempathy/gossip-paths.h> +#include <libempathy/gossip-debug.h> + +#include "gossip-ui-utils.h" +#include "gossip-stock.h" + +struct SizeData { + gint width; + gint height; + gboolean preserve_aspect_ratio; +}; + +static GladeXML * +get_glade_file (const gchar *filename, + const gchar *root, + const gchar *domain, + const gchar *first_required_widget, + va_list args) +{ + gchar *path; + GladeXML *gui; + const char *name; + GtkWidget **widget_ptr; + + path = gossip_paths_get_glade_path (filename); + gui = glade_xml_new (path, root, domain); + g_free (path); + + if (!gui) { + g_warning ("Couldn't find necessary glade file '%s'", filename); + return NULL; + } + + for (name = first_required_widget; name; name = va_arg (args, char *)) { + widget_ptr = va_arg (args, void *); + + *widget_ptr = glade_xml_get_widget (gui, name); + + if (!*widget_ptr) { + g_warning ("Glade file '%s' is missing widget '%s'.", + filename, name); + continue; + } + } + + return gui; +} + +void +gossip_glade_get_file_simple (const gchar *filename, + const gchar *root, + const gchar *domain, + const gchar *first_required_widget, ...) +{ + va_list args; + GladeXML *gui; + + va_start (args, first_required_widget); + + gui = get_glade_file (filename, + root, + domain, + first_required_widget, + args); + + va_end (args); + + if (!gui) { + return; + } + + g_object_unref (gui); +} + +GladeXML * +gossip_glade_get_file (const gchar *filename, + const gchar *root, + const gchar *domain, + const gchar *first_required_widget, ...) +{ + va_list args; + GladeXML *gui; + + va_start (args, first_required_widget); + + gui = get_glade_file (filename, + root, + domain, + first_required_widget, + args); + + va_end (args); + + if (!gui) { + return NULL; + } + + return gui; +} + +void +gossip_glade_connect (GladeXML *gui, + gpointer user_data, + gchar *first_widget, ...) +{ + va_list args; + const gchar *name; + const gchar *signal; + GtkWidget *widget; + gpointer *callback; + + va_start (args, first_widget); + + for (name = first_widget; name; name = va_arg (args, char *)) { + signal = va_arg (args, void *); + callback = va_arg (args, void *); + + widget = glade_xml_get_widget (gui, name); + if (!widget) { + g_warning ("Glade file is missing widget '%s', aborting", + name); + continue; + } + + g_signal_connect (widget, + signal, + G_CALLBACK (callback), + user_data); + } + + va_end (args); +} + +void +gossip_glade_setup_size_group (GladeXML *gui, + GtkSizeGroupMode mode, + gchar *first_widget, ...) +{ + va_list args; + GtkWidget *widget; + GtkSizeGroup *size_group; + const gchar *name; + + va_start (args, first_widget); + + size_group = gtk_size_group_new (mode); + + for (name = first_widget; name; name = va_arg (args, char *)) { + widget = glade_xml_get_widget (gui, name); + if (!widget) { + g_warning ("Glade file is missing widget '%s'", name); + continue; + } + + gtk_size_group_add_widget (size_group, widget); + } + + g_object_unref (size_group); + + va_end (args); +} + +GdkPixbuf * +gossip_pixbuf_from_smiley (GossipSmiley type, + GtkIconSize icon_size) +{ + GtkIconTheme *theme; + GdkPixbuf *pixbuf = NULL; + GError *error = NULL; + gint w, h; + gint size; + const gchar *icon_id; + + theme = gtk_icon_theme_get_default (); + + if (!gtk_icon_size_lookup (icon_size, &w, &h)) { + size = 16; + } else { + size = (w + h) / 2; + } + + switch (type) { + case GOSSIP_SMILEY_NORMAL: /* :) */ + icon_id = "stock_smiley-1"; + break; + case GOSSIP_SMILEY_WINK: /* ;) */ + icon_id = "stock_smiley-3"; + break; + case GOSSIP_SMILEY_BIGEYE: /* =) */ + icon_id = "stock_smiley-2"; + break; + case GOSSIP_SMILEY_NOSE: /* :-) */ + icon_id = "stock_smiley-7"; + break; + case GOSSIP_SMILEY_CRY: /* :'( */ + icon_id = "stock_smiley-11"; + break; + case GOSSIP_SMILEY_SAD: /* :( */ + icon_id = "stock_smiley-4"; + break; + case GOSSIP_SMILEY_SCEPTICAL: /* :/ */ + icon_id = "stock_smiley-9"; + break; + case GOSSIP_SMILEY_BIGSMILE: /* :D */ + icon_id = "stock_smiley-6"; + break; + case GOSSIP_SMILEY_INDIFFERENT: /* :| */ + icon_id = "stock_smiley-8"; + break; + case GOSSIP_SMILEY_TOUNGE: /* :p */ + icon_id = "stock_smiley-10"; + break; + case GOSSIP_SMILEY_SHOCKED: /* :o */ + icon_id = "stock_smiley-5"; + break; + case GOSSIP_SMILEY_COOL: /* 8) */ + icon_id = "stock_smiley-15"; + break; + case GOSSIP_SMILEY_SORRY: /* *| */ + icon_id = "stock_smiley-12"; + break; + case GOSSIP_SMILEY_KISS: /* :* */ + icon_id = "stock_smiley-13"; + break; + case GOSSIP_SMILEY_SHUTUP: /* :# */ + icon_id = "stock_smiley-14"; + break; + case GOSSIP_SMILEY_YAWN: /* |O */ + icon_id = ""; + break; + case GOSSIP_SMILEY_CONFUSED: /* :$ */ + icon_id = "stock_smiley-17"; + break; + case GOSSIP_SMILEY_ANGEL: /* O) */ + icon_id = "stock_smiley-18"; + break; + case GOSSIP_SMILEY_OOOH: /* :x */ + icon_id = "stock_smiley-19"; + break; + case GOSSIP_SMILEY_LOOKAWAY: /* *) */ + icon_id = "stock_smiley-20"; + break; + case GOSSIP_SMILEY_BLUSH: /* *S */ + icon_id = "stock_smiley-23"; + break; + case GOSSIP_SMILEY_COOLBIGSMILE: /* 8D */ + icon_id = "stock_smiley-25"; + break; + case GOSSIP_SMILEY_ANGRY: /* :@ */ + icon_id = "stock_smiley-16"; + break; + case GOSSIP_SMILEY_BOSS: /* @) */ + icon_id = "stock_smiley-21"; + break; + case GOSSIP_SMILEY_MONKEY: /* #) */ + icon_id = "stock_smiley-22"; + break; + case GOSSIP_SMILEY_SILLY: /* O) */ + icon_id = "stock_smiley-24"; + break; + case GOSSIP_SMILEY_SICK: /* +o( */ + icon_id = "stock_smiley-26"; + break; + + default: + g_assert_not_reached (); + icon_id = NULL; + } + + pixbuf = gtk_icon_theme_load_icon (theme, + icon_id, /* icon name */ + size, /* size */ + 0, /* flags */ + &error); + + return pixbuf; +} + +GdkPixbuf * +gossip_pixbuf_from_profile (McProfile *profile, + GtkIconSize icon_size) +{ + GtkIconTheme *theme; + gint size = 48; + gint w, h; + const gchar *icon_id = NULL; + GError *error = NULL; + + theme = gtk_icon_theme_get_default (); + + if (gtk_icon_size_lookup (icon_size, &w, &h)) { + size = (w + h) / 2; + } + + icon_id = mc_profile_get_icon_name (profile); + + theme = gtk_icon_theme_get_default (); + return gtk_icon_theme_load_icon (theme, + icon_id, /* Icon name */ + size, /* Size */ + 0, /* Flags */ + &error); +} + +GdkPixbuf * +gossip_pixbuf_from_account (McAccount *account, + GtkIconSize icon_size) +{ + McProfile *profile; + + profile = mc_account_get_profile (account); + + return gossip_pixbuf_from_profile (profile, icon_size); +} + +GdkPixbuf * +gossip_pixbuf_for_presence_state (GossipPresenceState state) +{ + const gchar *stock = NULL; + + switch (state) { + case GOSSIP_PRESENCE_STATE_AVAILABLE: + stock = GOSSIP_STOCK_AVAILABLE; + break; + case GOSSIP_PRESENCE_STATE_BUSY: + stock = GOSSIP_STOCK_BUSY; + break; + case GOSSIP_PRESENCE_STATE_AWAY: + stock = GOSSIP_STOCK_AWAY; + break; + case GOSSIP_PRESENCE_STATE_EXT_AWAY: + stock = GOSSIP_STOCK_EXT_AWAY; + break; + case GOSSIP_PRESENCE_STATE_HIDDEN: + case GOSSIP_PRESENCE_STATE_UNAVAILABLE: + stock = GOSSIP_STOCK_OFFLINE; + break; + } + + return gossip_stock_render (stock, GTK_ICON_SIZE_MENU); +} + +GdkPixbuf * +gossip_pixbuf_for_presence (GossipPresence *presence) +{ + GossipPresenceState state; + + g_return_val_if_fail (GOSSIP_IS_PRESENCE (presence), + gossip_pixbuf_offline ()); + + state = gossip_presence_get_state (presence); + + return gossip_pixbuf_for_presence_state (state); +} + +GdkPixbuf * +gossip_pixbuf_for_contact (GossipContact *contact) +{ + GossipPresence *presence; + GossipSubscription subscription; + + g_return_val_if_fail (GOSSIP_IS_CONTACT (contact), + gossip_pixbuf_offline ()); + + presence = gossip_contact_get_active_presence (contact); + + if (presence) { + return gossip_pixbuf_for_presence (presence); + } + + subscription = gossip_contact_get_subscription (contact); + + if (subscription != GOSSIP_SUBSCRIPTION_BOTH && + subscription != GOSSIP_SUBSCRIPTION_TO) { + return gossip_stock_render (GOSSIP_STOCK_PENDING, + GTK_ICON_SIZE_MENU); + } + + return gossip_pixbuf_offline (); +} + +GdkPixbuf * +gossip_pixbuf_offline (void) +{ + return gossip_stock_render (GOSSIP_STOCK_OFFLINE, + GTK_ICON_SIZE_MENU); +} + +GdkPixbuf * +gossip_pixbuf_avatar_from_contact (GossipContact *contact) +{ + GdkPixbuf *pixbuf; + GdkPixbufLoader *loader; + GossipAvatar *avatar; + GError *error = NULL; + + g_return_val_if_fail (GOSSIP_IS_CONTACT (contact), NULL); + + avatar = gossip_contact_get_avatar (contact); + if (!avatar) { + return NULL; + } + + loader = gdk_pixbuf_loader_new (); + + if (!gdk_pixbuf_loader_write (loader, avatar->data, avatar->len, &error)) { + g_warning ("Couldn't write avatar image:%p with " + "length:%" G_GSIZE_FORMAT " to pixbuf loader: %s", + avatar->data, avatar->len, error->message); + g_error_free (error); + return NULL; + } + + gdk_pixbuf_loader_close (loader, NULL); + + pixbuf = gdk_pixbuf_loader_get_pixbuf (loader); + + g_object_ref (pixbuf); + g_object_unref (loader); + + return pixbuf; +} + +static void +pixbuf_from_avatar_size_prepared_cb (GdkPixbufLoader *loader, + int width, + int height, + struct SizeData *data) +{ + g_return_if_fail (width > 0 && height > 0); + + if (data->preserve_aspect_ratio && (data->width > 0 || data->height > 0)) { + if (width < data->width && height < data->height) { + width = width; + height = height; + } + + if (data->width < 0) { + width = width * (double) data->height / (gdouble) height; + height = data->height; + } else if (data->height < 0) { + height = height * (double) data->width / (double) width; + width = data->width; + } else if ((double) height * (double) data->width > + (double) width * (double) data->height) { + width = 0.5 + (double) width * (double) data->height / (double) height; + height = data->height; + } else { + height = 0.5 + (double) height * (double) data->width / (double) width; + width = data->width; + } + } else { + if (data->width > 0) { + width = data->width; + } + + if (data->height > 0) { + height = data->height; + } + } + + gdk_pixbuf_loader_set_size (loader, width, height); +} + +GdkPixbuf * +gossip_pixbuf_from_avatar_scaled (GossipAvatar *avatar, + gint width, + gint height) +{ + GdkPixbuf *pixbuf; + GdkPixbufLoader *loader; + struct SizeData data; + GError *error = NULL; + + if (!avatar) { + return NULL; + } + + data.width = width; + data.height = height; + data.preserve_aspect_ratio = TRUE; + + loader = gdk_pixbuf_loader_new (); + + g_signal_connect (loader, "size-prepared", + G_CALLBACK (pixbuf_from_avatar_size_prepared_cb), + &data); + + if (!gdk_pixbuf_loader_write (loader, avatar->data, avatar->len, &error)) { + g_warning ("Couldn't write avatar image:%p with " + "length:%" G_GSIZE_FORMAT " to pixbuf loader: %s", + avatar->data, avatar->len, error->message); + g_error_free (error); + return NULL; + } + + gdk_pixbuf_loader_close (loader, NULL); + + pixbuf = gdk_pixbuf_loader_get_pixbuf (loader); + + g_object_ref (pixbuf); + g_object_unref (loader); + + return pixbuf; +} + +GdkPixbuf * +gossip_pixbuf_avatar_from_contact_scaled (GossipContact *contact, + gint width, + gint height) +{ + GossipAvatar *avatar; + + g_return_val_if_fail (GOSSIP_IS_CONTACT (contact), NULL); + + avatar = gossip_contact_get_avatar (contact); + + return gossip_pixbuf_from_avatar_scaled (avatar, width, height); +} +/* Stolen from GtkSourceView, hence the weird intendation. Please keep it like + * that to make it easier to apply changes from the original code. + */ +#define GTK_TEXT_UNKNOWN_CHAR 0xFFFC + +/* this function acts like g_utf8_offset_to_pointer() except that if it finds a + * decomposable character it consumes the decomposition length from the given + * offset. So it's useful when the offset was calculated for the normalized + * version of str, but we need a pointer to str itself. */ +static const gchar * +pointer_from_offset_skipping_decomp (const gchar *str, gint offset) +{ + gchar *casefold, *normal; + const gchar *p, *q; + + p = str; + while (offset > 0) + { + q = g_utf8_next_char (p); + casefold = g_utf8_casefold (p, q - p); + normal = g_utf8_normalize (casefold, -1, G_NORMALIZE_NFD); + offset -= g_utf8_strlen (normal, -1); + g_free (casefold); + g_free (normal); + p = q; + } + return p; +} + +static const gchar * +g_utf8_strcasestr (const gchar *haystack, const gchar *needle) +{ + gsize needle_len; + gsize haystack_len; + const gchar *ret = NULL; + gchar *p; + gchar *casefold; + gchar *caseless_haystack; + gint i; + + g_return_val_if_fail (haystack != NULL, NULL); + g_return_val_if_fail (needle != NULL, NULL); + + casefold = g_utf8_casefold (haystack, -1); + caseless_haystack = g_utf8_normalize (casefold, -1, G_NORMALIZE_NFD); + g_free (casefold); + + needle_len = g_utf8_strlen (needle, -1); + haystack_len = g_utf8_strlen (caseless_haystack, -1); + + if (needle_len == 0) + { + ret = (gchar *)haystack; + goto finally_1; + } + + if (haystack_len < needle_len) + { + ret = NULL; + goto finally_1; + } + + p = (gchar*)caseless_haystack; + needle_len = strlen (needle); + i = 0; + + while (*p) + { + if ((strncmp (p, needle, needle_len) == 0)) + { + ret = pointer_from_offset_skipping_decomp (haystack, i); + goto finally_1; + } + + p = g_utf8_next_char (p); + i++; + } + +finally_1: + g_free (caseless_haystack); + + return ret; +} + +static gboolean +g_utf8_caselessnmatch (const char *s1, const char *s2, + gssize n1, gssize n2) +{ + gchar *casefold; + gchar *normalized_s1; + gchar *normalized_s2; + gint len_s1; + gint len_s2; + gboolean ret = FALSE; + + g_return_val_if_fail (s1 != NULL, FALSE); + g_return_val_if_fail (s2 != NULL, FALSE); + g_return_val_if_fail (n1 > 0, FALSE); + g_return_val_if_fail (n2 > 0, FALSE); + + casefold = g_utf8_casefold (s1, n1); + normalized_s1 = g_utf8_normalize (casefold, -1, G_NORMALIZE_NFD); + g_free (casefold); + + casefold = g_utf8_casefold (s2, n2); + normalized_s2 = g_utf8_normalize (casefold, -1, G_NORMALIZE_NFD); + g_free (casefold); + + len_s1 = strlen (normalized_s1); + len_s2 = strlen (normalized_s2); + + if (len_s1 < len_s2) + goto finally_2; + + ret = (strncmp (normalized_s1, normalized_s2, len_s2) == 0); + +finally_2: + g_free (normalized_s1); + g_free (normalized_s2); + + return ret; +} + +static void +forward_chars_with_skipping (GtkTextIter *iter, + gint count, + gboolean skip_invisible, + gboolean skip_nontext, + gboolean skip_decomp) +{ + gint i; + + g_return_if_fail (count >= 0); + + i = count; + + while (i > 0) + { + gboolean ignored = FALSE; + + /* minimal workaround to avoid the infinite loop of bug #168247. + * It doesn't fix the problemjust the symptom... + */ + if (gtk_text_iter_is_end (iter)) + return; + + if (skip_nontext && gtk_text_iter_get_char (iter) == GTK_TEXT_UNKNOWN_CHAR) + ignored = TRUE; + + if (!ignored && skip_invisible && + /* _gtk_text_btree_char_is_invisible (iter)*/ FALSE) + ignored = TRUE; + + if (!ignored && skip_decomp) + { + /* being UTF8 correct sucks; this accounts for extra + offsets coming from canonical decompositions of + UTF8 characters (e.g. accented characters) which + g_utf8_normalize() performs */ + gchar *normal; + gchar buffer[6]; + gint buffer_len; + + buffer_len = g_unichar_to_utf8 (gtk_text_iter_get_char (iter), buffer); + normal = g_utf8_normalize (buffer, buffer_len, G_NORMALIZE_NFD); + i -= (g_utf8_strlen (normal, -1) - 1); + g_free (normal); + } + + gtk_text_iter_forward_char (iter); + + if (!ignored) + --i; + } +} + +static gboolean +lines_match (const GtkTextIter *start, + const gchar **lines, + gboolean visible_only, + gboolean slice, + GtkTextIter *match_start, + GtkTextIter *match_end) +{ + GtkTextIter next; + gchar *line_text; + const gchar *found; + gint offset; + + if (*lines == NULL || **lines == '\0') + { + if (match_start) + *match_start = *start; + if (match_end) + *match_end = *start; + return TRUE; + } + + next = *start; + gtk_text_iter_forward_line (&next); + + /* No more text in buffer, but *lines is nonempty */ + if (gtk_text_iter_equal (start, &next)) + return FALSE; + + if (slice) + { + if (visible_only) + line_text = gtk_text_iter_get_visible_slice (start, &next); + else + line_text = gtk_text_iter_get_slice (start, &next); + } + else + { + if (visible_only) + line_text = gtk_text_iter_get_visible_text (start, &next); + else + line_text = gtk_text_iter_get_text (start, &next); + } + + if (match_start) /* if this is the first line we're matching */ + { + found = g_utf8_strcasestr (line_text, *lines); + } + else + { + /* If it's not the first line, we have to match from the + * start of the line. + */ + if (g_utf8_caselessnmatch (line_text, *lines, strlen (line_text), + strlen (*lines))) + found = line_text; + else + found = NULL; + } + + if (found == NULL) + { + g_free (line_text); + return FALSE; + } + + /* Get offset to start of search string */ + offset = g_utf8_strlen (line_text, found - line_text); + + next = *start; + + /* If match start needs to be returned, set it to the + * start of the search string. + */ + forward_chars_with_skipping (&next, offset, visible_only, !slice, FALSE); + if (match_start) + { + *match_start = next; + } + + /* Go to end of search string */ + forward_chars_with_skipping (&next, g_utf8_strlen (*lines, -1), visible_only, !slice, TRUE); + + g_free (line_text); + + ++lines; + + if (match_end) + *match_end = next; + + /* pass NULL for match_start, since we don't need to find the + * start again. + */ + return lines_match (&next, lines, visible_only, slice, NULL, match_end); +} + +/* strsplit () that retains the delimiter as part of the string. */ +static gchar ** +strbreakup (const char *string, + const char *delimiter, + gint max_tokens) +{ + GSList *string_list = NULL, *slist; + gchar **str_array, *s, *casefold, *new_string; + guint i, n = 1; + + g_return_val_if_fail (string != NULL, NULL); + g_return_val_if_fail (delimiter != NULL, NULL); + + if (max_tokens < 1) + max_tokens = G_MAXINT; + + s = strstr (string, delimiter); + if (s) + { + guint delimiter_len = strlen (delimiter); + + do + { + guint len; + + len = s - string + delimiter_len; + new_string = g_new (gchar, len + 1); + strncpy (new_string, string, len); + new_string[len] = 0; + casefold = g_utf8_casefold (new_string, -1); + g_free (new_string); + new_string = g_utf8_normalize (casefold, -1, G_NORMALIZE_NFD); + g_free (casefold); + string_list = g_slist_prepend (string_list, new_string); + n++; + string = s + delimiter_len; + s = strstr (string, delimiter); + } while (--max_tokens && s); + } + + if (*string) + { + n++; + casefold = g_utf8_casefold (string, -1); + new_string = g_utf8_normalize (casefold, -1, G_NORMALIZE_NFD); + g_free (casefold); + string_list = g_slist_prepend (string_list, new_string); + } + + str_array = g_new (gchar*, n); + + i = n - 1; + + str_array[i--] = NULL; + for (slist = string_list; slist; slist = slist->next) + str_array[i--] = slist->data; + + g_slist_free (string_list); + + return str_array; +} + +gboolean +gossip_text_iter_forward_search (const GtkTextIter *iter, + const gchar *str, + GtkTextIter *match_start, + GtkTextIter *match_end, + const GtkTextIter *limit) +{ + gchar **lines = NULL; + GtkTextIter match; + gboolean retval = FALSE; + GtkTextIter search; + gboolean visible_only; + gboolean slice; + + g_return_val_if_fail (iter != NULL, FALSE); + g_return_val_if_fail (str != NULL, FALSE); + + if (limit && gtk_text_iter_compare (iter, limit) >= 0) + return FALSE; + + if (*str == '\0') { + /* If we can move one char, return the empty string there */ + match = *iter; + + if (gtk_text_iter_forward_char (&match)) { + if (limit && gtk_text_iter_equal (&match, limit)) { + return FALSE; + } + + if (match_start) { + *match_start = match; + } + if (match_end) { + *match_end = match; + } + return TRUE; + } else { + return FALSE; + } + } + + visible_only = TRUE; + slice = FALSE; + + /* locate all lines */ + lines = strbreakup (str, "\n", -1); + + search = *iter; + + do { + /* This loop has an inefficient worst-case, where + * gtk_text_iter_get_text () is called repeatedly on + * a single line. + */ + GtkTextIter end; + + if (limit && gtk_text_iter_compare (&search, limit) >= 0) { + break; + } + + if (lines_match (&search, (const gchar**)lines, + visible_only, slice, &match, &end)) { + if (limit == NULL || + (limit && gtk_text_iter_compare (&end, limit) <= 0)) { + retval = TRUE; + + if (match_start) { + *match_start = match; + } + if (match_end) { + *match_end = end; + } + } + break; + } + } while (gtk_text_iter_forward_line (&search)); + + g_strfreev ((gchar**)lines); + + return retval; +} + +static const gchar * +g_utf8_strrcasestr (const gchar *haystack, const gchar *needle) +{ + gsize needle_len; + gsize haystack_len; + const gchar *ret = NULL; + gchar *p; + gchar *casefold; + gchar *caseless_haystack; + gint i; + + g_return_val_if_fail (haystack != NULL, NULL); + g_return_val_if_fail (needle != NULL, NULL); + + casefold = g_utf8_casefold (haystack, -1); + caseless_haystack = g_utf8_normalize (casefold, -1, G_NORMALIZE_NFD); + g_free (casefold); + + needle_len = g_utf8_strlen (needle, -1); + haystack_len = g_utf8_strlen (caseless_haystack, -1); + + if (needle_len == 0) + { + ret = (gchar *)haystack; + goto finally_1; + } + + if (haystack_len < needle_len) + { + ret = NULL; + goto finally_1; + } + + i = haystack_len - needle_len; + p = g_utf8_offset_to_pointer (caseless_haystack, i); + needle_len = strlen (needle); + + while (p >= caseless_haystack) + { + if (strncmp (p, needle, needle_len) == 0) + { + ret = pointer_from_offset_skipping_decomp (haystack, i); + goto finally_1; + } + + p = g_utf8_prev_char (p); + i--; + } + +finally_1: + g_free (caseless_haystack); + + return ret; +} + +static gboolean +backward_lines_match (const GtkTextIter *start, + const gchar **lines, + gboolean visible_only, + gboolean slice, + GtkTextIter *match_start, + GtkTextIter *match_end) +{ + GtkTextIter line, next; + gchar *line_text; + const gchar *found; + gint offset; + + if (*lines == NULL || **lines == '\0') + { + if (match_start) + *match_start = *start; + if (match_end) + *match_end = *start; + return TRUE; + } + + line = next = *start; + if (gtk_text_iter_get_line_offset (&next) == 0) + { + if (!gtk_text_iter_backward_line (&next)) + return FALSE; + } + else + gtk_text_iter_set_line_offset (&next, 0); + + if (slice) + { + if (visible_only) + line_text = gtk_text_iter_get_visible_slice (&next, &line); + else + line_text = gtk_text_iter_get_slice (&next, &line); + } + else + { + if (visible_only) + line_text = gtk_text_iter_get_visible_text (&next, &line); + else + line_text = gtk_text_iter_get_text (&next, &line); + } + + if (match_start) /* if this is the first line we're matching */ + { + found = g_utf8_strrcasestr (line_text, *lines); + } + else + { + /* If it's not the first line, we have to match from the + * start of the line. + */ + if (g_utf8_caselessnmatch (line_text, *lines, strlen (line_text), + strlen (*lines))) + found = line_text; + else + found = NULL; + } + + if (found == NULL) + { + g_free (line_text); + return FALSE; + } + + /* Get offset to start of search string */ + offset = g_utf8_strlen (line_text, found - line_text); + + forward_chars_with_skipping (&next, offset, visible_only, !slice, FALSE); + + /* If match start needs to be returned, set it to the + * start of the search string. + */ + if (match_start) + { + *match_start = next; + } + + /* Go to end of search string */ + forward_chars_with_skipping (&next, g_utf8_strlen (*lines, -1), visible_only, !slice, TRUE); + + g_free (line_text); + + ++lines; + + if (match_end) + *match_end = next; + + /* try to match the rest of the lines forward, passing NULL + * for match_start so lines_match will try to match the entire + * line */ + return lines_match (&next, lines, visible_only, + slice, NULL, match_end); +} + +gboolean +gossip_text_iter_backward_search (const GtkTextIter *iter, + const gchar *str, + GtkTextIter *match_start, + GtkTextIter *match_end, + const GtkTextIter *limit) +{ + gchar **lines = NULL; + GtkTextIter match; + gboolean retval = FALSE; + GtkTextIter search; + gboolean visible_only; + gboolean slice; + + g_return_val_if_fail (iter != NULL, FALSE); + g_return_val_if_fail (str != NULL, FALSE); + + if (limit && gtk_text_iter_compare (iter, limit) <= 0) + return FALSE; + + if (*str == '\0') + { + /* If we can move one char, return the empty string there */ + match = *iter; + + if (gtk_text_iter_backward_char (&match)) + { + if (limit && gtk_text_iter_equal (&match, limit)) + return FALSE; + + if (match_start) + *match_start = match; + if (match_end) + *match_end = match; + return TRUE; + } + else + { + return FALSE; + } + } + + visible_only = TRUE; + slice = TRUE; + + /* locate all lines */ + lines = strbreakup (str, "\n", -1); + + search = *iter; + + while (TRUE) + { + /* This loop has an inefficient worst-case, where + * gtk_text_iter_get_text () is called repeatedly on + * a single line. + */ + GtkTextIter end; + + if (limit && gtk_text_iter_compare (&search, limit) <= 0) + break; + + if (backward_lines_match (&search, (const gchar**)lines, + visible_only, slice, &match, &end)) + { + if (limit == NULL || (limit && + gtk_text_iter_compare (&end, limit) > 0)) + { + retval = TRUE; + + if (match_start) + *match_start = match; + if (match_end) + *match_end = end; + } + break; + } + + if (gtk_text_iter_get_line_offset (&search) == 0) + { + if (!gtk_text_iter_backward_line (&search)) + break; + } + else + { + gtk_text_iter_set_line_offset (&search, 0); + } + } + + g_strfreev ((gchar**)lines); + + return retval; +} + +static gboolean +window_get_is_on_current_workspace (GtkWindow *window) +{ + GdkWindow *gdk_window; + + gdk_window = GTK_WIDGET (window)->window; + if (gdk_window) { + return !(gdk_window_get_state (gdk_window) & + GDK_WINDOW_STATE_ICONIFIED); + } else { + return FALSE; + } +} + +/* Checks if the window is visible as in visible on the current workspace. */ +gboolean +gossip_window_get_is_visible (GtkWindow *window) +{ + gboolean visible; + + g_return_val_if_fail (window != NULL, FALSE); + + g_object_get (window, + "visible", &visible, + NULL); + + return visible && window_get_is_on_current_workspace (window); +} + +/* Takes care of moving the window to the current workspace. */ +void +gossip_window_present (GtkWindow *window, + gboolean steal_focus) +{ + gboolean visible; + gboolean on_current; + guint32 timestamp; + + g_return_if_fail (window != NULL); + + /* There are three cases: hidden, visible, visible on another + * workspace. + */ + + g_object_get (window, + "visible", &visible, + NULL); + + on_current = window_get_is_on_current_workspace (window); + + if (visible && !on_current) { + /* Hide it so present brings it to the current workspace. */ + gtk_widget_hide (GTK_WIDGET (window)); + } + + timestamp = gtk_get_current_event_time (); + if (steal_focus && timestamp != GDK_CURRENT_TIME) { + gtk_window_present_with_time (window, timestamp); + } else { + gtk_window_present (window); + } +} + +/* The URL opening code can't handle schemeless strings, so we try to be + * smart and add http if there is no scheme or doesn't look like a mail + * address. This should work in most cases, and let us click on strings + * like "www.gnome.org". + */ +static gchar * +fixup_url (const gchar *url) +{ + gchar *real_url; + + if (!g_str_has_prefix (url, "http://") && + !strstr (url, ":/") && + !strstr (url, "@")) { + real_url = g_strdup_printf ("http://%s", url); + } else { + real_url = g_strdup (url); + } + + return real_url; +} + +void +gossip_url_show (const char *url) +{ + gchar *real_url; + GError *error = NULL; + + real_url = fixup_url (url); + gnome_url_show (real_url, &error); + if (error) { + g_warning ("Couldn't show URL:'%s'", real_url); + g_error_free (error); + } + + g_free (real_url); +} + +static void +link_button_hook (GtkLinkButton *button, + const gchar *link, + gpointer user_data) +{ + gossip_url_show (link); +} + +GtkWidget * +gossip_link_button_new (const gchar *url, + const gchar *title) +{ + static gboolean hook = FALSE; + + if (!hook) { + hook = TRUE; + gtk_link_button_set_uri_hook (link_button_hook, NULL, NULL); + } + + return gtk_link_button_new_with_label (url, title); +} + +/* FIXME: Do this in a proper way at some point, probably in GTK+? */ +void +gossip_window_set_default_icon_name (const gchar *name) +{ + gtk_window_set_default_icon_name (name); +} + +void +gossip_toggle_button_set_state_quietly (GtkWidget *widget, + GCallback callback, + gpointer user_data, + gboolean active) +{ + g_return_if_fail (GTK_IS_TOGGLE_BUTTON (widget)); + + g_signal_handlers_block_by_func (widget, callback, user_data); + g_object_set (widget, "active", active, NULL); + g_signal_handlers_unblock_by_func (widget, callback, user_data); +} + diff --git a/libempathy-gtk/gossip-ui-utils.h b/libempathy-gtk/gossip-ui-utils.h new file mode 100644 index 00000000..581587a7 --- /dev/null +++ b/libempathy-gtk/gossip-ui-utils.h @@ -0,0 +1,115 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2002-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Mikael Hallendal <micke@imendio.com> + * Richard Hult <richard@imendio.com> + * Martyn Russell <martyn@imendio.com> + * + * Part of this file is copied from GtkSourceView (gtksourceiter.c): + * Paolo Maggi + * Jeroen Zwartepoorte + */ + +#ifndef __GOSSIP_UI_UTILS_H__ +#define __GOSSIP_UI_UTILS_H__ + +#include <gtk/gtk.h> +#include <glade/glade.h> + +#include <libmissioncontrol/mc-account.h> +#include <libmissioncontrol/mc-profile.h> + +#include <libempathy/gossip-presence.h> +#include <libempathy/gossip-contact.h> +#include <libempathy/gossip-avatar.h> + +#include "gossip-chat-view.h" + +G_BEGIN_DECLS + +#define G_STR_EMPTY(x) ((x) == NULL || (x)[0] == '\0') + +/* Glade */ +void gossip_glade_get_file_simple (const gchar *filename, + const gchar *root, + const gchar *domain, + const gchar *first_required_widget, + ...); +GladeXML * gossip_glade_get_file (const gchar *filename, + const gchar *root, + const gchar *domain, + const gchar *first_required_widget, + ...); +void gossip_glade_connect (GladeXML *gui, + gpointer user_data, + gchar *first_widget, + ...); +void gossip_glade_setup_size_group (GladeXML *gui, + GtkSizeGroupMode mode, + gchar *first_widget, + ...); +/* Pixbufs */ +GdkPixbuf * gossip_pixbuf_from_smiley (GossipSmiley type, + GtkIconSize icon_size); +GdkPixbuf * gossip_pixbuf_from_profile (McProfile *account, + GtkIconSize icon_size); +GdkPixbuf * gossip_pixbuf_from_account (McAccount *account, + GtkIconSize icon_size); +GdkPixbuf * gossip_pixbuf_for_presence_state (GossipPresenceState state); +GdkPixbuf * gossip_pixbuf_for_presence (GossipPresence *presence); +GdkPixbuf * gossip_pixbuf_for_contact (GossipContact *contact); +GdkPixbuf * gossip_pixbuf_offline (void); +GdkPixbuf * gossip_pixbuf_from_avatar_scaled (GossipAvatar *avatar, + gint width, + gint height); +GdkPixbuf * gossip_pixbuf_avatar_from_contact (GossipContact *contact); +GdkPixbuf * gossip_pixbuf_avatar_from_contact_scaled (GossipContact *contact, + gint width, + gint height); +/* Text view */ +gboolean gossip_text_iter_forward_search (const GtkTextIter *iter, + const gchar *str, + GtkTextIter *match_start, + GtkTextIter *match_end, + const GtkTextIter *limit); +gboolean gossip_text_iter_backward_search (const GtkTextIter *iter, + const gchar *str, + GtkTextIter *match_start, + GtkTextIter *match_end, + const GtkTextIter *limit); + +/* Windows */ +gboolean gossip_window_get_is_visible (GtkWindow *window); +void gossip_window_present (GtkWindow *window, + gboolean steal_focus); +void gossip_window_set_default_icon_name (const gchar *name); + +void gossip_url_show (const char *url); + +void gossip_toggle_button_set_state_quietly (GtkWidget *widget, + GCallback callback, + gpointer user_data, + gboolean active); +GtkWidget *gossip_link_button_new (const gchar *url, + const gchar *title); + + +G_END_DECLS + +#endif /* __GOSSIP_UI_UTILS_H__ */ diff --git a/libempathy/Makefile.am b/libempathy/Makefile.am new file mode 100644 index 00000000..30d40c59 --- /dev/null +++ b/libempathy/Makefile.am @@ -0,0 +1,54 @@ +AM_CPPFLAGS = \ + -I. \ + -I$(top_srcdir) \ + -DDATADIR=\""$(datadir)"\" \ + -DLOCALEDIR=\""$(datadir)/locale"\" \ + $(EMPATHY_CFLAGS) \ + $(WARN_CFLAGS) + +BUILT_SOURCES = \ + empathy-marshal.h \ + empathy-marshal.c \ + empathy-chandler-glue.h + +noinst_LTLIBRARIES = libempathy.la + +libempathy_la_SOURCES = \ + gossip-conf.c gossip-conf.h \ + gossip-contact.c gossip-contact.h \ + gossip-avatar.c gossip-avatar.h \ + gossip-time.c gossip-time.h \ + gossip-presence.c gossip-presence.h \ + gossip-telepathy-group.c gossip-telepathy-group.h \ + gossip-paths.c gossip-paths.h \ + gossip-debug.c gossip-debug.h \ + gossip-utils.c gossip-utils.h \ + gossip-message.c gossip-message.h \ + empathy-session.c empathy-session.h \ + empathy-contact-list.c empathy-contact-list.h \ + empathy-contact-manager.c empathy-contact-manager.h \ + empathy-tp-chat.c empathy-tp-chat.h \ + empathy-chandler.c empathy-chandler.h \ + empathy-marshal-main.c + +libempathy_la_LIBADD = \ + $(EMPATHY_LIBS) + +libempathy_includedir = $(includedir)/empathy/ + +%-marshal.h: %-marshal.list Makefile.am + $(GLIB_GENMARSHAL) --header --prefix=$(subst -,_,$*)_marshal $< > $*-marshal.h + +%-marshal.c: %-marshal.list Makefile.am + $(GLIB_GENMARSHAL) --body --prefix=$(subst -,_,$*)_marshal $< > $*-marshal.c + +%-marshal-main.c: %-marshal.c %-marshal.h + +empathy-chandler-glue.h: empathy-chandler.xml + $(LIBTOOL) --mode=execute $(DBUS_BINDING_TOOL) --prefix=empathy_chandler --mode=glib-server --output=$@ $< + +EXTRA_DIST = \ + empathy-marshal.list \ + empathy-chandler.xml + +CLEANFILES = $(BUILT_SOURCES) diff --git a/libempathy/empathy-chandler.c b/libempathy/empathy-chandler.c new file mode 100644 index 00000000..adc415a9 --- /dev/null +++ b/libempathy/empathy-chandler.c @@ -0,0 +1,150 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2007 Xavier Claessens <xclaesse@gmail.com> + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#include <config.h> + +#include <dbus/dbus-glib.h> + +#include <libtelepathy/tp-helpers.h> +#include <libtelepathy/tp-conn.h> +#include <libtelepathy/tp-chan.h> + +#include "empathy-chandler.h" +#include "gossip-debug.h" +#include "empathy-marshal.h" + +#define DEBUG_DOMAIN "EmpathyChandler" + +static gboolean empathy_chandler_handle_channel (EmpathyChandler *chandler, + const gchar *bus_name, + const gchar *connection, + const gchar *channel_type, + const gchar *channel, + guint handle_type, + guint handle, + GError **error); + +#include "empathy-chandler-glue.h" + +enum { + NEW_CHANNEL, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +G_DEFINE_TYPE (EmpathyChandler, empathy_chandler, G_TYPE_OBJECT) + +static void +empathy_chandler_class_init (EmpathyChandlerClass *klass) +{ + signals[NEW_CHANNEL] = + g_signal_new ("new-channel", + G_OBJECT_CLASS_TYPE (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + empathy_marshal_VOID__OBJECT_OBJECT, + G_TYPE_NONE, + 2, TELEPATHY_CONN_TYPE, TELEPATHY_CHAN_TYPE); +} + +static void +empathy_chandler_init (EmpathyChandler *chandler) +{ +} + +EmpathyChandler * +empathy_chandler_new (const gchar *bus_name, + const gchar *object_path) +{ + static gboolean initialized = FALSE; + EmpathyChandler *chandler; + DBusGProxy *proxy; + guint result; + GError *error = NULL; + + if (!initialized) { + dbus_g_object_type_install_info (EMPATHY_TYPE_CHANDLER, + &dbus_glib_empathy_chandler_object_info); + initialized = TRUE; + } + + proxy = dbus_g_proxy_new_for_name (tp_get_bus (), + DBUS_SERVICE_DBUS, + DBUS_PATH_DBUS, + DBUS_INTERFACE_DBUS); + + if (!dbus_g_proxy_call (proxy, "RequestName", &error, + G_TYPE_STRING, bus_name, + G_TYPE_UINT, DBUS_NAME_FLAG_DO_NOT_QUEUE, + G_TYPE_INVALID, + G_TYPE_UINT, &result, + G_TYPE_INVALID)) { + gossip_debug (DEBUG_DOMAIN, + "Failed to request name: %s", + error ? error->message : "No error given"); + g_clear_error (&error); + + return NULL; + } + g_object_unref (proxy); + + chandler = g_object_new (EMPATHY_TYPE_CHANDLER, NULL); + dbus_g_connection_register_g_object (tp_get_bus (), + object_path, + G_OBJECT (chandler)); + + return chandler; +} + +static gboolean +empathy_chandler_handle_channel (EmpathyChandler *chandler, + const gchar *bus_name, + const gchar *connection, + const gchar *channel_type, + const gchar *channel, + guint handle_type, + guint handle, + GError **error) +{ + TpChan *tp_chan; + TpConn *tp_conn; + + tp_conn = tp_conn_new (tp_get_bus (), + bus_name, + connection); + + tp_chan = tp_chan_new (tp_get_bus(), + bus_name, + channel, + channel_type, + handle_type, + handle); + + g_signal_emit (chandler, signals[NEW_CHANNEL], 0, tp_conn, tp_chan); + + g_object_unref (tp_chan); + g_object_unref (tp_conn); + + return TRUE; +} + diff --git a/libempathy/empathy-chandler.h b/libempathy/empathy-chandler.h new file mode 100644 index 00000000..ca271613 --- /dev/null +++ b/libempathy/empathy-chandler.h @@ -0,0 +1,53 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2007 Xavier Claessens <xclaesse@gmail.com> + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + */ + +#ifndef __EMPATHY_CHANDLER_H__ +#define __EMPATHY_CHANDLER_H__ + +#include <glib.h> + +G_BEGIN_DECLS + +#define EMPATHY_TYPE_CHANDLER (empathy_chandler_get_type ()) +#define EMPATHY_CHANDLER(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), EMPATHY_TYPE_CHANDLER, EmpathyChandler)) +#define EMPATHY_CHANDLER_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), EMPATHY_TYPE_CHANDLER, EmpathyChandlerClass)) +#define EMPATHY_IS_CHANDLER(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), EMPATHY_TYPE_CHANDLER)) +#define EMPATHY_IS_CHANDLER_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), EMPATHY_TYPE_CHANDLER)) +#define EMPATHY_CHANDLER_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), EMPATHY_TYPE_CHANDLER, EmpathyChandlerClass)) + +typedef struct _EmpathyChandler EmpathyChandler; +typedef struct _EmpathyChandlerClass EmpathyChandlerClass; + +struct _EmpathyChandler { + GObject parent; +}; + +struct _EmpathyChandlerClass { + GObjectClass parent_class; +}; + +GType empathy_chandler_get_type (void) G_GNUC_CONST; +EmpathyChandler *empathy_chandler_new (const gchar *bus_name, + const gchar *object_path); + +G_END_DECLS + +#endif /* __EMPATHY_CHANDLER_H__ */ diff --git a/libempathy/empathy-chandler.xml b/libempathy/empathy-chandler.xml new file mode 100644 index 00000000..0fb264e5 --- /dev/null +++ b/libempathy/empathy-chandler.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<node name="/"> + <interface name="org.freedesktop.Telepathy.ChannelHandler"> + <method name="HandleChannel"> + <arg direction="in" type="s" name="bus_name" /> + <arg direction="in" type="o" name="connection" /> + <arg direction="in" type="s" name="channel_type" /> + <arg direction="in" type="o" name="channel" /> + <arg direction="in" type="u" name="handle_type" /> + <arg direction="in" type="u" name="handle" /> + </method> + </interface> +</node> diff --git a/libempathy/empathy-contact-list.c b/libempathy/empathy-contact-list.c new file mode 100644 index 00000000..2cc486de --- /dev/null +++ b/libempathy/empathy-contact-list.c @@ -0,0 +1,1793 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2007 Xavier Claessens <xclaesse@gmail.com> + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include <config.h> + +#include <string.h> + +#include <libtelepathy/tp-conn.h> +#include <libtelepathy/tp-chan.h> +#include <libtelepathy/tp-helpers.h> +#include <libtelepathy/tp-chan-type-contact-list-gen.h> +#include <libtelepathy/tp-conn-iface-aliasing-gen.h> +#include <libtelepathy/tp-conn-iface-presence-gen.h> +#include <libtelepathy/tp-conn-iface-avatars-gen.h> + +#include "empathy-contact-list.h" +#include "empathy-session.h" +#include "gossip-debug.h" +#include "gossip-telepathy-group.h" + +#define GET_PRIV(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), \ + EMPATHY_TYPE_CONTACT_LIST, EmpathyContactListPriv)) + +#define DEBUG_DOMAIN "ContactList" +#define MAX_AVATAR_REQUESTS 10 + +struct _EmpathyContactListPriv { + TpConn *tp_conn; + McAccount *account; + GossipContact *own_contact; + + GossipTelepathyGroup *known; + GossipTelepathyGroup *publish; + GossipTelepathyGroup *subscribe; + + GHashTable *groups; + GHashTable *contacts; + + DBusGProxy *aliasing_iface; + DBusGProxy *avatars_iface; + DBusGProxy *presence_iface; + + GList *avatar_requests_queue; +}; + +typedef enum { + CONTACT_LIST_TYPE_KNOWN, + CONTACT_LIST_TYPE_PUBLISH, + CONTACT_LIST_TYPE_SUBSCRIBE, + CONTACT_LIST_TYPE_UNKNOWN, + CONTACT_LIST_TYPE_COUNT +} ContactListType; + +typedef struct { + guint handle; + GList *new_groups; +} ContactListData; + +typedef struct { + EmpathyContactList *list; + guint handle; +} ContactListAvatarRequestData; + +typedef struct { + EmpathyContactList *list; + guint *handles; +} ContactListAliasesRequestData; + +static void empathy_contact_list_class_init (EmpathyContactListClass *klass); +static void empathy_contact_list_init (EmpathyContactList *list); +static void contact_list_finalize (GObject *object); +static void contact_list_finalize_proxies (EmpathyContactList *list); +static void contact_list_contact_removed_foreach (guint handle, + GossipContact *contact, + EmpathyContactList *list); +static void contact_list_destroy_cb (DBusGProxy *proxy, + EmpathyContactList *list); +static gboolean contact_list_find_foreach (guint handle, + GossipContact *contact, + gchar *id); +static void contact_list_newchannel_cb (DBusGProxy *proxy, + const gchar *object_path, + const gchar *channel_type, + TelepathyHandleType handle_type, + guint channel_handle, + gboolean suppress_handle, + EmpathyContactList *list); +static ContactListType contact_list_get_type (EmpathyContactList *list, + TpChan *list_chan); +static void contact_list_contact_added_cb (GossipTelepathyGroup *group, + GArray *handles, + guint actor_handle, + guint reason, + const gchar *message, + EmpathyContactList *list); +static void contact_list_contact_removed_cb (GossipTelepathyGroup *group, + GArray *handles, + guint actor_handle, + guint reason, + const gchar *message, + EmpathyContactList *list); +static void contact_list_local_pending_cb (GossipTelepathyGroup *group, + GArray *handles, + guint actor_handle, + guint reason, + const gchar *message, + EmpathyContactList *list); +static void contact_list_groups_updated_cb (GossipContact *contact, + GParamSpec *param, + EmpathyContactList *list); +static void contact_list_subscription_updated_cb (GossipContact *contact, + GParamSpec *param, + EmpathyContactList *list); +static void contact_list_name_updated_cb (GossipContact *contact, + GParamSpec *param, + EmpathyContactList *list); +static void contact_list_update_groups_foreach (gchar *object_path, + GossipTelepathyGroup *group, + ContactListData *data); +static GossipTelepathyGroup * contact_list_get_group (EmpathyContactList *list, + const gchar *name); +static gboolean contact_list_find_group (gchar *key, + GossipTelepathyGroup *group, + gchar *group_name); +static void contact_list_get_groups_foreach (gchar *key, + GossipTelepathyGroup *group, + GList **groups); +static void contact_list_group_channel_closed_cb (TpChan *channel, + EmpathyContactList *list); +static void contact_list_group_members_added_cb (GossipTelepathyGroup *group, + GArray *members, + guint actor_handle, + guint reason, + const gchar *message, + EmpathyContactList *list); +static void contact_list_group_members_removed_cb (GossipTelepathyGroup *group, + GArray *members, + guint actor_handle, + guint reason, + const gchar *message, + EmpathyContactList *list); +static void contact_list_get_contacts_foreach (guint handle, + GossipContact *contact, + GList **contacts); +static void contact_list_get_info (EmpathyContactList *list, + GArray *handles); +static void contact_list_request_avatar (EmpathyContactList *list, + guint handle); +static void contact_list_start_avatar_requests (EmpathyContactList *list); +static void contact_list_avatar_update_cb (DBusGProxy *proxy, + guint handle, + gchar *new_token, + EmpathyContactList *list); +static void contact_list_request_avatar_cb (DBusGProxy *proxy, + GArray *avatar_data, + gchar *mime_type, + GError *error, + ContactListAvatarRequestData *data); +static void contact_list_aliases_update_cb (DBusGProxy *proxy, + GPtrArray *handlers, + EmpathyContactList *list); +static void contact_list_request_aliases_cb (DBusGProxy *proxy, + gchar **contact_names, + GError *error, + ContactListAliasesRequestData *data); +static void contact_list_presence_update_cb (DBusGProxy *proxy, + GHashTable *handle_table, + EmpathyContactList *list); +static void contact_list_parse_presence_foreach (guint handle, + GValueArray *presence_struct, + EmpathyContactList *list); +static void contact_list_presences_table_foreach (const gchar *state_str, + GHashTable *presences_table, + GossipPresence **presence); +static GossipPresenceState contact_list_presence_state_from_str (const gchar *str); + +enum { + CONTACT_ADDED, + CONTACT_REMOVED, + DESTROY, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; +static guint n_avatar_requests = 0; + +G_DEFINE_TYPE (EmpathyContactList, empathy_contact_list, G_TYPE_OBJECT); + +static void +empathy_contact_list_class_init (EmpathyContactListClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = contact_list_finalize; + + signals[CONTACT_ADDED] = + g_signal_new ("contact-added", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, + 1, GOSSIP_TYPE_CONTACT); + + signals[CONTACT_REMOVED] = + g_signal_new ("contact-removed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, + 1, GOSSIP_TYPE_CONTACT); + + signals[DESTROY] = + g_signal_new ("destroy", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); + + g_type_class_add_private (object_class, sizeof (EmpathyContactListPriv)); +} + +static void +empathy_contact_list_init (EmpathyContactList *list) +{ + EmpathyContactListPriv *priv; + + priv = GET_PRIV (list); + + priv->groups = g_hash_table_new_full (g_str_hash, + g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) g_object_unref); + priv->contacts = g_hash_table_new_full (g_direct_hash, + g_direct_equal, + NULL, + (GDestroyNotify) g_object_unref); +} + + +static void +contact_list_finalize (GObject *object) +{ + EmpathyContactListPriv *priv; + EmpathyContactList *list; + + list = EMPATHY_CONTACT_LIST (object); + priv = GET_PRIV (list); + + gossip_debug (DEBUG_DOMAIN, "finalize: %p", object); + + if (priv->tp_conn) { + contact_list_finalize_proxies (list); + g_object_unref (priv->tp_conn); + } + + if (priv->known) { + g_object_unref (priv->known); + } + + if (priv->subscribe) { + g_object_unref (priv->subscribe); + } + + if (priv->publish) { + g_object_unref (priv->publish); + } + + g_object_unref (priv->account); + g_object_unref (priv->own_contact); + g_hash_table_destroy (priv->groups); + g_hash_table_destroy (priv->contacts); + + G_OBJECT_CLASS (empathy_contact_list_parent_class)->finalize (object); +} + +EmpathyContactList * +empathy_contact_list_new (McAccount *account) +{ + EmpathyContactListPriv *priv; + EmpathyContactList *list; + MissionControl *mc; + TpConn *tp_conn; + guint handle; + GError *error = NULL; + + g_return_val_if_fail (MC_IS_ACCOUNT (account), NULL); + + mc = empathy_session_get_mission_control (); + + if (mission_control_get_connection_status (mc, account, NULL) != 0) { + /* The account is not connected, nothing to do. */ + return NULL; + } + + tp_conn = mission_control_get_connection (mc, account, NULL); + g_return_val_if_fail (tp_conn != NULL, NULL); + + list = g_object_new (EMPATHY_TYPE_CONTACT_LIST, NULL); + priv = GET_PRIV (list); + + priv->tp_conn = tp_conn; + priv->account = g_object_ref (account); + + g_signal_connect (priv->tp_conn, "destroy", + G_CALLBACK (contact_list_destroy_cb), + list); + + priv->aliasing_iface = tp_conn_get_interface (priv->tp_conn, + TELEPATHY_CONN_IFACE_ALIASING_QUARK); + priv->avatars_iface = tp_conn_get_interface (priv->tp_conn, + TELEPATHY_CONN_IFACE_AVATARS_QUARK); + priv->presence_iface = tp_conn_get_interface (priv->tp_conn, + TELEPATHY_CONN_IFACE_PRESENCE_QUARK); + + if (priv->aliasing_iface) { + dbus_g_proxy_connect_signal (priv->aliasing_iface, + "AliasesChanged", + G_CALLBACK (contact_list_aliases_update_cb), + list, NULL); + } + + if (priv->avatars_iface) { + dbus_g_proxy_connect_signal (priv->avatars_iface, + "AvatarUpdated", + G_CALLBACK (contact_list_avatar_update_cb), + list, NULL); + } + + if (priv->presence_iface) { + dbus_g_proxy_connect_signal (priv->presence_iface, + "PresenceUpdate", + G_CALLBACK (contact_list_presence_update_cb), + list, NULL); + } + + /* Get our own handle and contact */ + if (!tp_conn_get_self_handle (DBUS_G_PROXY (priv->tp_conn), + &handle, &error)) { + gossip_debug (DEBUG_DOMAIN, "GetSelfHandle Error: %s", + error ? error->message : "No error given"); + g_clear_error (&error); + } else { + priv->own_contact = empathy_contact_list_get_from_handle (list, handle); + } + + return list; +} + +void +empathy_contact_list_setup (EmpathyContactList *list) +{ + EmpathyContactListPriv *priv; + GPtrArray *channels; + GError *error = NULL; + guint i; + + g_return_if_fail (EMPATHY_IS_CONTACT_LIST (list)); + + priv = GET_PRIV (list); + + dbus_g_proxy_connect_signal (DBUS_G_PROXY (priv->tp_conn), "NewChannel", + G_CALLBACK (contact_list_newchannel_cb), + list, NULL); + + /* Get existing channels */ + if (!tp_conn_list_channels (DBUS_G_PROXY (priv->tp_conn), + &channels, + &error)) { + gossip_debug (DEBUG_DOMAIN, + "Failed to get list of open channels: %s", + error ? error->message : "No error given"); + g_clear_error (&error); + return; + } + + for (i = 0; channels->len > i; i++) { + GValueArray *chan_struct; + const gchar *object_path; + const gchar *chan_iface; + TelepathyHandleType handle_type; + guint handle; + + chan_struct = g_ptr_array_index (channels, i); + object_path = g_value_get_boxed (g_value_array_get_nth (chan_struct, 0)); + chan_iface = g_value_get_string (g_value_array_get_nth (chan_struct, 1)); + handle_type = g_value_get_uint (g_value_array_get_nth (chan_struct, 2)); + handle = g_value_get_uint (g_value_array_get_nth (chan_struct, 3)); + + contact_list_newchannel_cb (DBUS_G_PROXY (priv->tp_conn), + object_path, chan_iface, + handle_type, handle, + FALSE, list); + + g_value_array_free (chan_struct); + } + + g_ptr_array_free (channels, TRUE); +} + +McAccount * +empathy_contact_list_get_account (EmpathyContactList *list) +{ + EmpathyContactListPriv *priv; + + g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST (list), NULL); + + priv = GET_PRIV (list); + + return priv->account; +} + +GossipContact * +empathy_contact_list_get_own (EmpathyContactList *list) +{ + EmpathyContactListPriv *priv; + + g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST (list), NULL); + + priv = GET_PRIV (list); + + return priv->own_contact; +} + +GossipContact * +empathy_contact_list_find (EmpathyContactList *list, + const gchar *id) +{ + EmpathyContactListPriv *priv; + GossipContact *contact; + + g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST (list), NULL); + + priv = GET_PRIV (list); + + contact = g_hash_table_find (priv->contacts, + (GHRFunc) contact_list_find_foreach, + (gchar*) id); + + return NULL; +} + +void +empathy_contact_list_add (EmpathyContactList *list, + guint handle, + const gchar *message) +{ + EmpathyContactListPriv *priv; + + g_return_if_fail (EMPATHY_IS_CONTACT_LIST (list)); + + priv = GET_PRIV (list); + + gossip_telepathy_group_add_member (priv->subscribe, handle, message); +} + +void +empathy_contact_list_remove (EmpathyContactList *list, + guint handle) +{ + EmpathyContactListPriv *priv; + + g_return_if_fail (EMPATHY_IS_CONTACT_LIST (list)); + + priv = GET_PRIV (list); + + gossip_telepathy_group_remove_member (priv->subscribe, handle, ""); + gossip_telepathy_group_remove_member (priv->publish, handle, ""); + gossip_telepathy_group_remove_member (priv->known, handle, ""); +} + +GossipContact * +empathy_contact_list_get_from_id (EmpathyContactList *list, + const gchar *id) +{ + EmpathyContactListPriv *priv; + GossipContact *contact; + const gchar *contact_ids[] = {id, NULL}; + GArray *handles; + guint handle; + GError *error = NULL; + + g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST (list), NULL); + g_return_val_if_fail (id != NULL, NULL); + + priv = GET_PRIV (list); + + contact = empathy_contact_list_find (list, id); + if (contact) { + return contact; + } + + /* The id is unknown, requests a new handle */ + if (!tp_conn_request_handles (DBUS_G_PROXY (priv->tp_conn), + TP_HANDLE_TYPE_CONTACT, + contact_ids, + &handles, &error)) { + gossip_debug (DEBUG_DOMAIN, + "RequestHandle for %s failed: %s", id, + error ? error->message : "No error given"); + g_clear_error (&error); + return 0; + } + + handle = g_array_index(handles, guint, 0); + g_array_free (handles, TRUE); + + return empathy_contact_list_get_from_handle (list, handle); +} + +GossipContact * +empathy_contact_list_get_from_handle (EmpathyContactList *list, + guint handle) +{ + GossipContact *contact; + GArray *handles; + GList *contacts; + + g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST (list), NULL); + + handles = g_array_new (FALSE, FALSE, sizeof (guint)); + g_array_append_val (handles, handle); + + contacts = empathy_contact_list_get_from_handles (list, handles); + g_array_free (handles, TRUE); + + if (!contacts) { + return NULL; + } + + contact = contacts->data; + g_list_free (contacts); + + return contact; +} + +GList * +empathy_contact_list_get_from_handles (EmpathyContactList *list, + GArray *handles) +{ + EmpathyContactListPriv *priv; + gchar **handles_names; + gchar **id; + GArray *new_handles; + GList *contacts = NULL; + guint i; + GError *error = NULL; + + g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST (list), NULL); + g_return_val_if_fail (handles != NULL, NULL); + + priv = GET_PRIV (list); + + /* Search all handles we already have */ + new_handles = g_array_new (FALSE, FALSE, sizeof (guint)); + for (i = 0; i < handles->len; i++) { + GossipContact *contact; + guint handle; + + handle = g_array_index (handles, guint, i); + contact = g_hash_table_lookup (priv->contacts, + GUINT_TO_POINTER (handle)); + + if (contact) { + contacts = g_list_prepend (contacts, + g_object_ref (contact)); + } else { + g_array_append_val (new_handles, handle); + } + } + + if (new_handles->len == 0) { + return contacts; + } + + /* Holds all handles we don't have yet. + * FIXME: We should release them at some point. */ + if (!tp_conn_hold_handles (DBUS_G_PROXY (priv->tp_conn), + TP_HANDLE_TYPE_CONTACT, + new_handles, &error)) { + gossip_debug (DEBUG_DOMAIN, + "HoldHandles Error: %s", + error ? error->message : "No error given"); + g_clear_error (&error); + g_array_free (new_handles, TRUE); + return contacts; + } + + /* Get the IDs of all new handles */ + if (!tp_conn_inspect_handles (DBUS_G_PROXY (priv->tp_conn), + TP_HANDLE_TYPE_CONTACT, + new_handles, + &handles_names, + &error)) { + gossip_debug (DEBUG_DOMAIN, + "InspectHandle Error: %s", + error ? error->message : "No error given"); + g_clear_error (&error); + g_array_free (new_handles, TRUE); + return contacts; + } + + /* Create contact objects */ + for (i = 0, id = handles_names; *id && i < new_handles->len; id++, i++) { + GossipContact *contact; + guint handle; + + handle = g_array_index (new_handles, guint, i); + contact = g_object_new (GOSSIP_TYPE_CONTACT, + "account", priv->account, + "id", *id, + "handle", handle, + NULL); + + g_signal_connect (contact, "notify::groups", + G_CALLBACK (contact_list_groups_updated_cb), + list); + g_signal_connect (contact, "notify::subscription", + G_CALLBACK (contact_list_subscription_updated_cb), + list); + g_signal_connect (contact, "notify::name", + G_CALLBACK (contact_list_name_updated_cb), + list); + + gossip_debug (DEBUG_DOMAIN, "new contact created: %s (%d)", + *id, handle); + + g_hash_table_insert (priv->contacts, + GUINT_TO_POINTER (handle), + contact); + + contacts = g_list_prepend (contacts, g_object_ref (contact)); + } + + contact_list_get_info (list, new_handles); + + g_array_free (new_handles, TRUE); + g_strfreev (handles_names); + + return contacts; +} + +void +empathy_contact_list_rename_group (EmpathyContactList *list, + const gchar *old_group, + const gchar *new_group) +{ + EmpathyContactListPriv *priv; + GossipTelepathyGroup *group; + GArray *members; + + g_return_if_fail (EMPATHY_IS_CONTACT_LIST (list)); + g_return_if_fail (old_group != NULL); + g_return_if_fail (new_group != NULL); + + priv = GET_PRIV (list); + + group = g_hash_table_find (priv->groups, + (GHRFunc) contact_list_find_group, + (gchar*) old_group); + if (!group) { + /* The group doesn't exists on this account */ + return; + } + + gossip_debug (DEBUG_DOMAIN, "rename group %s to %s", group, new_group); + + /* Remove all members from the old group */ + members = gossip_telepathy_group_get_members (group); + gossip_telepathy_group_remove_members (group, members, ""); + contact_list_group_members_removed_cb (group, members, + 0, + TP_CHANNEL_GROUP_CHANGE_REASON_NONE, + NULL, list); + g_hash_table_remove (priv->groups, + gossip_telepathy_group_get_object_path (group)); + + /* Add all members to the new group */ + group = contact_list_get_group (list, new_group); + if (group) { + gossip_telepathy_group_add_members (group, members, ""); + } +} + +GList * +empathy_contact_list_get_groups (EmpathyContactList *list) +{ + EmpathyContactListPriv *priv; + GList *groups = NULL; + + g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST (list), NULL); + + priv = GET_PRIV (list); + + g_hash_table_foreach (priv->groups, + (GHFunc) contact_list_get_groups_foreach, + &groups); + + groups = g_list_sort (groups, (GCompareFunc) strcmp); + + return groups; +} + +GList * +empathy_contact_list_get_contacts (EmpathyContactList *list) +{ + EmpathyContactListPriv *priv; + GList *contacts = NULL; + + g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST (list), NULL); + + priv = GET_PRIV (list); + + /* FIXME: we should only return contacts that are in the contact list */ + g_hash_table_foreach (priv->contacts, + (GHFunc) contact_list_get_contacts_foreach, + &contacts); + + return contacts; +} + +static void +contact_list_finalize_proxies (EmpathyContactList *list) +{ + EmpathyContactListPriv *priv; + + priv = GET_PRIV (list); + + g_signal_handlers_disconnect_by_func (priv->tp_conn, + contact_list_destroy_cb, + list); + dbus_g_proxy_disconnect_signal (DBUS_G_PROXY (priv->tp_conn), "NewChannel", + G_CALLBACK (contact_list_newchannel_cb), + list); + + if (priv->aliasing_iface) { + dbus_g_proxy_disconnect_signal (priv->aliasing_iface, + "AliasesChanged", + G_CALLBACK (contact_list_aliases_update_cb), + list); + } + + if (priv->avatars_iface) { + dbus_g_proxy_disconnect_signal (priv->avatars_iface, + "AvatarUpdated", + G_CALLBACK (contact_list_avatar_update_cb), + list); + } + + if (priv->presence_iface) { + dbus_g_proxy_disconnect_signal (priv->presence_iface, + "PresenceUpdate", + G_CALLBACK (contact_list_presence_update_cb), + list); + } +} + +static void +contact_list_destroy_cb (DBusGProxy *proxy, + EmpathyContactList *list) +{ + EmpathyContactListPriv *priv; + + priv = GET_PRIV (list); + + gossip_debug (DEBUG_DOMAIN, "Connection destroyed. " + "Account disconnected or CM crashed."); + + /* DBus proxies should NOT be used anymore */ + g_object_unref (priv->tp_conn); + priv->tp_conn = NULL; + priv->aliasing_iface = NULL; + priv->avatars_iface = NULL; + priv->presence_iface = NULL; + + /* Remove all contacts */ + g_hash_table_foreach (priv->contacts, + (GHFunc) contact_list_contact_removed_foreach, + list); + g_hash_table_remove_all (priv->contacts); + + /* Tell the world to not use us anymore */ + g_signal_emit (list, signals[DESTROY], 0); +} + +static void +contact_list_contact_removed_foreach (guint handle, + GossipContact *contact, + EmpathyContactList *list) +{ + g_signal_handlers_disconnect_by_func (contact, + contact_list_groups_updated_cb, + list); + g_signal_handlers_disconnect_by_func (contact, + contact_list_subscription_updated_cb, + list); + g_signal_handlers_disconnect_by_func (contact, + contact_list_name_updated_cb, + list); + + g_signal_emit (list, signals[CONTACT_REMOVED], 0, contact); +} + +static void +contact_list_block_contact (EmpathyContactList *list, + GossipContact *contact) +{ + g_signal_handlers_block_by_func (contact, + contact_list_groups_updated_cb, + list); + g_signal_handlers_block_by_func (contact, + contact_list_subscription_updated_cb, + list); + g_signal_handlers_block_by_func (contact, + contact_list_name_updated_cb, + list); +} + +static void +contact_list_unblock_contact (EmpathyContactList *list, + GossipContact *contact) +{ + g_signal_handlers_unblock_by_func (contact, + contact_list_groups_updated_cb, + list); + g_signal_handlers_unblock_by_func (contact, + contact_list_subscription_updated_cb, + list); + g_signal_handlers_unblock_by_func (contact, + contact_list_name_updated_cb, + list); +} + +static gboolean +contact_list_find_foreach (guint handle, + GossipContact *contact, + gchar *id) +{ + if (strcmp (gossip_contact_get_id (contact), id) == 0) { + return TRUE; + } + + return FALSE; +} + +static void +contact_list_newchannel_cb (DBusGProxy *proxy, + const gchar *object_path, + const gchar *channel_type, + TelepathyHandleType handle_type, + guint channel_handle, + gboolean suppress_handle, + EmpathyContactList *list) +{ + EmpathyContactListPriv *priv; + GossipTelepathyGroup *group; + TpChan *new_chan; + const gchar *bus_name; + GArray *members; + + priv = GET_PRIV (list); + + if (strcmp (channel_type, TP_IFACE_CHANNEL_TYPE_CONTACT_LIST) != 0 || + suppress_handle) { + return; + } + + bus_name = dbus_g_proxy_get_bus_name (DBUS_G_PROXY (priv->tp_conn)); + new_chan = tp_chan_new (tp_get_bus (), + bus_name, + object_path, + channel_type, handle_type, channel_handle); + + if (handle_type == TP_HANDLE_TYPE_LIST) { + ContactListType list_type; + + list_type = contact_list_get_type (list, new_chan); + if (list_type == CONTACT_LIST_TYPE_UNKNOWN) { + gossip_debug (DEBUG_DOMAIN, "Unknown contact list channel"); + g_object_unref (new_chan); + return; + } + + gossip_debug (DEBUG_DOMAIN, "New contact list channel of type: %d", + list_type); + + group = gossip_telepathy_group_new (new_chan, priv->tp_conn); + + switch (list_type) { + case CONTACT_LIST_TYPE_KNOWN: + if (priv->known) { + g_object_unref (priv->known); + } + priv->known = group; + break; + case CONTACT_LIST_TYPE_PUBLISH: + if (priv->publish) { + g_object_unref (priv->publish); + } + priv->publish = group; + break; + case CONTACT_LIST_TYPE_SUBSCRIBE: + if (priv->subscribe) { + g_object_unref (priv->subscribe); + } + priv->subscribe = group; + break; + default: + g_assert_not_reached (); + } + + /* Connect and setup the new contact-list group */ + if (list_type == CONTACT_LIST_TYPE_KNOWN || + list_type == CONTACT_LIST_TYPE_SUBSCRIBE) { + g_signal_connect (group, "members-added", + G_CALLBACK (contact_list_contact_added_cb), + list); + g_signal_connect (group, "members-removed", + G_CALLBACK (contact_list_contact_removed_cb), + list); + + members = gossip_telepathy_group_get_members (group); + contact_list_contact_added_cb (group, members, 0, + TP_CHANNEL_GROUP_CHANGE_REASON_NONE, + NULL, list); + g_array_free (members, TRUE); + } + if (list_type == CONTACT_LIST_TYPE_PUBLISH) { + GPtrArray *info; + GArray *pending; + guint i; + + g_signal_connect (group, "local-pending", + G_CALLBACK (contact_list_local_pending_cb), + list); + + info = gossip_telepathy_group_get_local_pending_members_with_info (group); + + if (!info) { + /* This happens with butterfly because + * GetLocalPendingMembersWithInfo is not + * implemented */ + g_object_unref (new_chan); + return; + } + + pending = g_array_sized_new (FALSE, FALSE, sizeof (guint), 1); + for (i = 0; info->len > i; i++) { + GValueArray *pending_struct; + guint member; + guint invitor; + guint reason; + const gchar *message; + + pending_struct = g_ptr_array_index (info, i); + member = g_value_get_uint (g_value_array_get_nth (pending_struct, 0)); + invitor = g_value_get_uint (g_value_array_get_nth (pending_struct, 1)); + reason = g_value_get_uint (g_value_array_get_nth (pending_struct, 2)); + message = g_value_get_string (g_value_array_get_nth (pending_struct, 3)); + + g_array_insert_val (pending, 0, member); + + contact_list_local_pending_cb (group, pending, + invitor, + reason, + message, list); + + g_value_array_free (pending_struct); + } + + g_ptr_array_free (info, TRUE); + g_array_free (pending, TRUE); + } + } + else if (handle_type == TP_HANDLE_TYPE_GROUP) { + const gchar *object_path; + + object_path = dbus_g_proxy_get_path (DBUS_G_PROXY (new_chan)); + if (g_hash_table_lookup (priv->groups, object_path)) { + g_object_unref (new_chan); + return; + } + + group = gossip_telepathy_group_new (new_chan, priv->tp_conn); + + gossip_debug (DEBUG_DOMAIN, "New server-side group channel: %s", + gossip_telepathy_group_get_name (group)); + + dbus_g_proxy_connect_signal (DBUS_G_PROXY (new_chan), "Closed", + G_CALLBACK + (contact_list_group_channel_closed_cb), + list, NULL); + + g_hash_table_insert (priv->groups, g_strdup (object_path), group); + g_signal_connect (group, "members-added", + G_CALLBACK (contact_list_group_members_added_cb), + list); + g_signal_connect (group, "members-removed", + G_CALLBACK (contact_list_group_members_removed_cb), + list); + + members = gossip_telepathy_group_get_members (group); + contact_list_group_members_added_cb (group, members, 0, + TP_CHANNEL_GROUP_CHANGE_REASON_NONE, + NULL, list); + g_array_free (members, TRUE); + } + + g_object_unref (new_chan); +} + +static ContactListType +contact_list_get_type (EmpathyContactList *list, + TpChan *list_chan) +{ + EmpathyContactListPriv *priv; + GArray *handles; + gchar **handle_name; + ContactListType list_type; + GError *error = NULL; + + priv = GET_PRIV (list); + + handles = g_array_new (FALSE, FALSE, sizeof (guint)); + g_array_append_val (handles, list_chan->handle); + + if (!tp_conn_inspect_handles (DBUS_G_PROXY (priv->tp_conn), + TP_HANDLE_TYPE_LIST, + handles, + &handle_name, + &error)) { + gossip_debug (DEBUG_DOMAIN, + "InspectHandle Error: %s", + error ? error->message : "No error given"); + g_clear_error (&error); + g_array_free (handles, TRUE); + return CONTACT_LIST_TYPE_UNKNOWN; + } + + if (strcmp (*handle_name, "subscribe") == 0) { + list_type = CONTACT_LIST_TYPE_SUBSCRIBE; + } else if (strcmp (*handle_name, "publish") == 0) { + list_type = CONTACT_LIST_TYPE_PUBLISH; + } else if (strcmp (*handle_name, "known") == 0) { + list_type = CONTACT_LIST_TYPE_KNOWN; + } else { + list_type = CONTACT_LIST_TYPE_UNKNOWN; + } + + g_strfreev (handle_name); + g_array_free (handles, TRUE); + + return list_type; +} + +static void +contact_list_contact_added_cb (GossipTelepathyGroup *group, + GArray *handles, + guint actor_handle, + guint reason, + const gchar *message, + EmpathyContactList *list) +{ + EmpathyContactListPriv *priv; + GList *added_list, *l; + + priv = GET_PRIV (list); + + added_list = empathy_contact_list_get_from_handles (list, handles); + + for (l = added_list; l; l = l->next) { + GossipContact *contact; + + contact = GOSSIP_CONTACT (l->data); + contact_list_block_contact (list, contact); + gossip_contact_set_subscription (contact, GOSSIP_SUBSCRIPTION_BOTH); + contact_list_unblock_contact (list, contact); + + g_signal_emit (list, signals[CONTACT_ADDED], 0, contact); + + g_object_unref (contact); + } + + g_list_free (added_list); +} + +static void +contact_list_contact_removed_cb (GossipTelepathyGroup *group, + GArray *handles, + guint actor_handle, + guint reason, + const gchar *message, + EmpathyContactList *list) +{ + EmpathyContactListPriv *priv; + GList *removed_list, *l; + + priv = GET_PRIV (list); + + removed_list = empathy_contact_list_get_from_handles (list, handles); + + for (l = removed_list; l; l = l->next) { + GossipContact *contact; + guint handle; + + contact = GOSSIP_CONTACT (l->data); + + handle = gossip_contact_get_handle (contact); + g_hash_table_remove (priv->contacts, GUINT_TO_POINTER (handle)); + + g_signal_emit (list, signals[CONTACT_REMOVED], 0, contact); + + g_object_unref (contact); + } + + g_list_free (removed_list); +} + +static void +contact_list_local_pending_cb (GossipTelepathyGroup *group, + GArray *handles, + guint actor_handle, + guint reason, + const gchar *message, + EmpathyContactList *list) +{ + EmpathyContactListPriv *priv; + GList *pending_list, *l; + + priv = GET_PRIV (list); + + pending_list = empathy_contact_list_get_from_handles (list, handles); + + for (l = pending_list; l; l = l->next) { + GossipContact *contact; + + contact = GOSSIP_CONTACT (l->data); + + /* FIXME: Is that the correct way ? */ + contact_list_block_contact (list, contact); + gossip_contact_set_subscription (contact, GOSSIP_SUBSCRIPTION_FROM); + contact_list_unblock_contact (list, contact); + g_signal_emit (list, signals[CONTACT_ADDED], 0, contact); + + g_object_unref (contact); + } + + g_list_free (pending_list); +} + +static void +contact_list_groups_updated_cb (GossipContact *contact, + GParamSpec *param, + EmpathyContactList *list) +{ + EmpathyContactListPriv *priv; + ContactListData data; + GList *groups, *l; + + priv = GET_PRIV (list); + + /* Make sure all groups are created */ + groups = gossip_contact_get_groups (contact); + for (l = groups; l; l = l->next) { + contact_list_get_group (list, l->data); + } + + data.handle = gossip_contact_get_handle (contact); + data.new_groups = groups; + + g_hash_table_foreach (priv->groups, + (GHFunc) contact_list_update_groups_foreach, + &data); +} + +static void +contact_list_subscription_updated_cb (GossipContact *contact, + GParamSpec *param, + EmpathyContactList *list) +{ + EmpathyContactListPriv *priv; + GossipSubscription subscription; + guint handle; + + priv = GET_PRIV (list); + + subscription = gossip_contact_get_subscription (contact); + handle = gossip_contact_get_handle (contact); + + /* FIXME: what to do here, I'm a bit lost... */ + if (subscription) { + gossip_telepathy_group_add_member (priv->publish, handle, ""); + } else { + gossip_telepathy_group_remove_member (priv->publish, handle, ""); + } +} + +static void +contact_list_name_updated_cb (GossipContact *contact, + GParamSpec *param, + EmpathyContactList *list) +{ + EmpathyContactListPriv *priv; + GHashTable *new_alias; + const gchar *new_name; + guint handle; + GError *error = NULL; + + priv = GET_PRIV (list); + + handle = gossip_contact_get_handle (contact); + new_name = gossip_contact_get_name (contact); + + gossip_debug (DEBUG_DOMAIN, "renaming handle %d to %s", + handle, new_name); + + new_alias = g_hash_table_new_full (g_direct_hash, + g_direct_equal, + NULL, + g_free); + + g_hash_table_insert (new_alias, + GUINT_TO_POINTER (handle), + g_strdup (new_name)); + + if (!tp_conn_iface_aliasing_set_aliases (priv->aliasing_iface, + new_alias, + &error)) { + gossip_debug (DEBUG_DOMAIN, + "Couldn't rename contact: %s", + error ? error->message : "No error given"); + g_clear_error (&error); + } + + g_hash_table_destroy (new_alias); +} + +static void +contact_list_update_groups_foreach (gchar *object_path, + GossipTelepathyGroup *group, + ContactListData *data) +{ + gboolean is_member; + gboolean found = FALSE; + const gchar *group_name; + GList *l; + + is_member = gossip_telepathy_group_is_member (group, data->handle); + group_name = gossip_telepathy_group_get_name (group); + + for (l = data->new_groups; l; l = l->next) { + if (strcmp (group_name, l->data) == 0) { + found = TRUE; + break; + } + } + + if (is_member && !found) { + /* We are no longer member of this group */ + gossip_debug (DEBUG_DOMAIN, "Contact %d removed from group '%s'", + data->handle, group_name); + gossip_telepathy_group_remove_member (group, data->handle, ""); + } + + if (!is_member && found) { + /* We are now member of this group */ + gossip_debug (DEBUG_DOMAIN, "Contact %d added to group '%s'", + data->handle, group_name); + gossip_telepathy_group_add_member (group, data->handle, ""); + } +} + +static GossipTelepathyGroup * +contact_list_get_group (EmpathyContactList *list, + const gchar *name) +{ + EmpathyContactListPriv *priv; + GossipTelepathyGroup *group; + TpChan *group_channel; + GArray *handles; + guint group_handle; + char *group_object_path; + const char *names[2] = {name, NULL}; + GError *error = NULL; + + priv = GET_PRIV (list); + + group = g_hash_table_find (priv->groups, + (GHRFunc) contact_list_find_group, + (gchar*) name); + if (group) { + return group; + } + + gossip_debug (DEBUG_DOMAIN, "creating new group: %s", name); + + if (!tp_conn_request_handles (DBUS_G_PROXY (priv->tp_conn), + TP_HANDLE_TYPE_GROUP, + names, + &handles, + &error)) { + gossip_debug (DEBUG_DOMAIN, + "Couldn't request the creation of a new handle for group: %s", + error ? error->message : "No error given"); + g_clear_error (&error); + return NULL; + } + group_handle = g_array_index (handles, guint, 0); + g_array_free (handles, TRUE); + + if (!tp_conn_request_channel (DBUS_G_PROXY (priv->tp_conn), + TP_IFACE_CHANNEL_TYPE_CONTACT_LIST, + TP_HANDLE_TYPE_GROUP, + group_handle, + FALSE, + &group_object_path, + &error)) { + gossip_debug (DEBUG_DOMAIN, + "Couldn't request the creation of a new group channel: %s", + error ? error->message : "No error given"); + g_clear_error (&error); + return NULL; + } + + group_channel = tp_chan_new (tp_get_bus (), + dbus_g_proxy_get_bus_name (DBUS_G_PROXY (priv->tp_conn)), + group_object_path, + TP_IFACE_CHANNEL_TYPE_CONTACT_LIST, + TP_HANDLE_TYPE_GROUP, + group_handle); + + dbus_g_proxy_connect_signal (DBUS_G_PROXY (group_channel), + "Closed", + G_CALLBACK + (contact_list_group_channel_closed_cb), + list, + NULL); + + group = gossip_telepathy_group_new (group_channel, priv->tp_conn); + g_hash_table_insert (priv->groups, group_object_path, group); + g_signal_connect (group, "members-added", + G_CALLBACK (contact_list_group_members_added_cb), + list); + g_signal_connect (group, "members-removed", + G_CALLBACK (contact_list_group_members_removed_cb), + list); + + return group; +} + +static gboolean +contact_list_find_group (gchar *key, + GossipTelepathyGroup *group, + gchar *group_name) +{ + if (strcmp (group_name, gossip_telepathy_group_get_name (group)) == 0) { + return TRUE; + } + + return FALSE; +} + +static void +contact_list_get_groups_foreach (gchar *key, + GossipTelepathyGroup *group, + GList **groups) +{ + const gchar *name; + + name = gossip_telepathy_group_get_name (group); + *groups = g_list_append (*groups, g_strdup (name)); +} + +static void +contact_list_group_channel_closed_cb (TpChan *channel, + EmpathyContactList *list) +{ + EmpathyContactListPriv *priv; + + priv = GET_PRIV (list); + + g_hash_table_remove (priv->groups, + dbus_g_proxy_get_path (DBUS_G_PROXY (channel))); +} + +static void +contact_list_group_members_added_cb (GossipTelepathyGroup *group, + GArray *members, + guint actor_handle, + guint reason, + const gchar *message, + EmpathyContactList *list) +{ + EmpathyContactListPriv *priv; + GList *added_list, *l; + const gchar *group_name; + + priv = GET_PRIV (list); + + group_name = gossip_telepathy_group_get_name (group); + added_list = empathy_contact_list_get_from_handles (list, members); + + for (l = added_list; l; l = l->next) { + GossipContact *contact; + GList *contact_groups; + + contact = GOSSIP_CONTACT (l->data); + contact_groups = gossip_contact_get_groups (contact); + + if (!g_list_find_custom (contact_groups, + group_name, + (GCompareFunc) strcmp)) { + gossip_debug (DEBUG_DOMAIN, "Contact %s added to group '%s'", + gossip_contact_get_name (contact), + group_name); + contact_groups = g_list_append (contact_groups, + g_strdup (group_name)); + contact_list_block_contact (list, contact); + gossip_contact_set_groups (contact, contact_groups); + contact_list_unblock_contact (list, contact); + } + + g_object_unref (contact); + } + + g_list_free (added_list); +} + +static void +contact_list_group_members_removed_cb (GossipTelepathyGroup *group, + GArray *members, + guint actor_handle, + guint reason, + const gchar *message, + EmpathyContactList *list) +{ + EmpathyContactListPriv *priv; + GList *removed_list, *l; + const gchar *group_name; + + priv = GET_PRIV (list); + + group_name = gossip_telepathy_group_get_name (group); + removed_list = empathy_contact_list_get_from_handles (list, members); + + for (l = removed_list; l; l = l->next) { + GossipContact *contact; + GList *contact_groups; + GList *to_remove; + + /* FIXME: Does it leak ? */ + contact = GOSSIP_CONTACT (l->data); + contact_groups = gossip_contact_get_groups (contact); + contact_groups = g_list_copy (contact_groups); + + to_remove = g_list_find_custom (contact_groups, + group_name, + (GCompareFunc) strcmp); + if (to_remove) { + gossip_debug (DEBUG_DOMAIN, "Contact %d removed from group '%s'", + gossip_contact_get_handle (contact), + group_name); + contact_groups = g_list_remove_link (contact_groups, + to_remove); + contact_list_block_contact (list, contact); + gossip_contact_set_groups (contact, contact_groups); + contact_list_unblock_contact (list, contact); + } + + g_list_free (contact_groups); + + g_object_unref (contact); + } + + g_list_free (removed_list); +} + +static void +contact_list_get_contacts_foreach (guint handle, + GossipContact *contact, + GList **contacts) +{ + *contacts = g_list_append (*contacts, g_object_ref (contact)); +} + +static void +contact_list_get_info (EmpathyContactList *list, + GArray *handles) +{ + EmpathyContactListPriv *priv; + GError *error = NULL; + + priv = GET_PRIV (list); + + if (priv->presence_iface) { + /* FIXME: We should use GetPresence instead */ + if (!tp_conn_iface_presence_request_presence (priv->presence_iface, + handles, &error)) { + gossip_debug (DEBUG_DOMAIN, + "Could not request presences: %s", + error ? error->message : "No error given"); + g_clear_error (&error); + } + } + + if (priv->aliasing_iface) { + ContactListAliasesRequestData *data; + + data = g_slice_new (ContactListAliasesRequestData); + data->list = list; + data->handles = g_memdup (handles->data, handles->len * sizeof (guint)); + + tp_conn_iface_aliasing_request_aliases_async (priv->aliasing_iface, + handles, + (tp_conn_iface_aliasing_request_aliases_reply) + contact_list_request_aliases_cb, + data); + } + + if (priv->avatars_iface) { + guint i; + + for (i = 0; i < handles->len; i++) { + guint handle; + + handle = g_array_index (handles, gint, i); + contact_list_request_avatar (list, handle); + } + } +} + +static void +contact_list_request_avatar (EmpathyContactList *list, + guint handle) +{ + EmpathyContactListPriv *priv; + + priv = GET_PRIV (list); + + /* We queue avatar requests to not send too many dbus async + * calls at once. If we don't we reach the dbus's limit of + * pending calls */ + priv->avatar_requests_queue = g_list_append (priv->avatar_requests_queue, + GUINT_TO_POINTER (handle)); + contact_list_start_avatar_requests (list); +} + +static void +contact_list_start_avatar_requests (EmpathyContactList *list) +{ + EmpathyContactListPriv *priv; + ContactListAvatarRequestData *data; + + priv = GET_PRIV (list); + + while (n_avatar_requests < MAX_AVATAR_REQUESTS && + priv->avatar_requests_queue) { + data = g_slice_new (ContactListAvatarRequestData); + data->list = list; + data->handle = GPOINTER_TO_UINT (priv->avatar_requests_queue->data); + + n_avatar_requests++; + priv->avatar_requests_queue = g_list_remove (priv->avatar_requests_queue, + priv->avatar_requests_queue->data); + + tp_conn_iface_avatars_request_avatar_async (priv->avatars_iface, + data->handle, + (tp_conn_iface_avatars_request_avatar_reply) + contact_list_request_avatar_cb, + data); + } +} + +static void +contact_list_avatar_update_cb (DBusGProxy *proxy, + guint handle, + gchar *new_token, + EmpathyContactList *list) +{ + gossip_debug (DEBUG_DOMAIN, "Changing avatar for %d to %s", + handle, new_token); + + contact_list_request_avatar (list, handle); +} + +static void +contact_list_request_avatar_cb (DBusGProxy *proxy, + GArray *avatar_data, + gchar *mime_type, + GError *error, + ContactListAvatarRequestData *data) +{ + GossipContact *contact; + + contact = empathy_contact_list_get_from_handle (data->list, data->handle); + + if (error) { + gossip_debug (DEBUG_DOMAIN, "Error requesting avatar for %s: %s", + gossip_contact_get_name (contact), + error ? error->message : "No error given"); + } else { + GossipAvatar *avatar; + + avatar = gossip_avatar_new (avatar_data->data, + avatar_data->len, + mime_type); + contact_list_block_contact (data->list, contact); + gossip_contact_set_avatar (contact, avatar); + contact_list_unblock_contact (data->list, contact); + gossip_avatar_unref (avatar); + } + + n_avatar_requests--; + contact_list_start_avatar_requests (data->list); + + g_slice_free (ContactListAvatarRequestData, data); +} + +static void +contact_list_aliases_update_cb (DBusGProxy *proxy, + GPtrArray *renamed_handlers, + EmpathyContactList *list) +{ + gint i; + + for (i = 0; renamed_handlers->len > i; i++) { + guint handle; + const gchar *alias; + GValueArray *renamed_struct; + GossipContact *contact; + + renamed_struct = g_ptr_array_index (renamed_handlers, i); + handle = g_value_get_uint(g_value_array_get_nth (renamed_struct, 0)); + alias = g_value_get_string(g_value_array_get_nth (renamed_struct, 1)); + + if (alias && *alias == '\0') { + alias = NULL; + } + + contact = empathy_contact_list_get_from_handle (list, handle); + contact_list_block_contact (list, contact); + gossip_contact_set_name (contact, alias); + contact_list_unblock_contact (list, contact); + + gossip_debug (DEBUG_DOMAIN, "contact %d renamed to %s (update cb)", + handle, alias); + } +} + +static void +contact_list_request_aliases_cb (DBusGProxy *proxy, + gchar **contact_names, + GError *error, + ContactListAliasesRequestData *data) +{ + guint i = 0; + gchar **name; + + for (name = contact_names; *name && !error; name++) { + GossipContact *contact; + + contact = empathy_contact_list_get_from_handle (data->list, + data->handles[i]); + contact_list_block_contact (data->list, contact); + gossip_contact_set_name (contact, *name); + contact_list_unblock_contact (data->list, contact); + + gossip_debug (DEBUG_DOMAIN, "contact %d renamed to %s (request cb)", + data->handles[i], *name); + + i++; + } + + g_free (data->handles); + g_slice_free (ContactListAliasesRequestData, data); +} + +static void +contact_list_presence_update_cb (DBusGProxy *proxy, + GHashTable *handle_table, + EmpathyContactList *list) +{ + g_hash_table_foreach (handle_table, + (GHFunc) contact_list_parse_presence_foreach, + list); +} + +static void +contact_list_parse_presence_foreach (guint handle, + GValueArray *presence_struct, + EmpathyContactList *list) +{ + GHashTable *presences_table; + GossipContact *contact; + GossipPresence *presence = NULL; + + contact = empathy_contact_list_get_from_handle (list, handle); + presences_table = g_value_get_boxed (g_value_array_get_nth (presence_struct, 1)); + + g_hash_table_foreach (presences_table, + (GHFunc) contact_list_presences_table_foreach, + &presence); + + gossip_debug (DEBUG_DOMAIN, "Presence changed for %s (%d)", + gossip_contact_get_name (contact), + handle); + + contact_list_block_contact (list, contact); + if (presence) { + gossip_contact_add_presence (contact, presence); + g_object_unref (presence); + } else { + g_object_set (contact, "presences", NULL, NULL); + } + contact_list_unblock_contact (list, contact); +} + +static void +contact_list_presences_table_foreach (const gchar *state_str, + GHashTable *presences_table, + GossipPresence **presence) +{ + GossipPresenceState state; + const GValue *message; + + if (*presence) { + g_object_unref (*presence); + *presence = NULL; + } + + state = contact_list_presence_state_from_str (state_str); + if (state == GOSSIP_PRESENCE_STATE_UNAVAILABLE) { + return; + } + + *presence = gossip_presence_new (); + gossip_presence_set_state (*presence, state); + + message = g_hash_table_lookup (presences_table, "message"); + if (message != NULL) { + gossip_presence_set_status (*presence, + g_value_get_string (message)); + } + + gossip_presence_set_resource (*presence, ""); +} + +static GossipPresenceState +contact_list_presence_state_from_str (const gchar *str) +{ + if (strcmp (str, "available") == 0) { + return GOSSIP_PRESENCE_STATE_AVAILABLE; + } else if ((strcmp (str, "dnd") == 0) || (strcmp (str, "busy") == 0)) { + return GOSSIP_PRESENCE_STATE_BUSY; + } else if ((strcmp (str, "away") == 0) || (strcmp (str, "brb") == 0)) { + return GOSSIP_PRESENCE_STATE_AWAY; + } else if (strcmp (str, "xa") == 0) { + return GOSSIP_PRESENCE_STATE_EXT_AWAY; + } else if (strcmp (str, "hidden") == 0) { + return GOSSIP_PRESENCE_STATE_HIDDEN; + } else if (strcmp (str, "offline") == 0) { + return GOSSIP_PRESENCE_STATE_UNAVAILABLE; + } else if (strcmp (str, "chat") == 0) { + /* We don't support chat, so treat it like available. */ + return GOSSIP_PRESENCE_STATE_AVAILABLE; + } + + return GOSSIP_PRESENCE_STATE_AVAILABLE; +} + diff --git a/libempathy/empathy-contact-list.h b/libempathy/empathy-contact-list.h new file mode 100644 index 00000000..db8b36e3 --- /dev/null +++ b/libempathy/empathy-contact-list.h @@ -0,0 +1,76 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2007 Xavier Claessens <xclaesse@gmail.com> + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __EMPATHY_CONTACT_LIST_H__ +#define __EMPATHY_CONTACT_LIST_H__ + +#include <glib.h> +#include <libmissioncontrol/mc-account.h> + +#include "gossip-contact.h" + +G_BEGIN_DECLS + +#define EMPATHY_TYPE_CONTACT_LIST (empathy_contact_list_get_type ()) +#define EMPATHY_CONTACT_LIST(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), EMPATHY_TYPE_CONTACT_LIST, EmpathyContactList)) +#define EMPATHY_CONTACT_LIST_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), EMPATHY_TYPE_CONTACT_LIST, EmpathyContactListClass)) +#define EMPATHY_IS_CONTACT_LIST(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), EMPATHY_TYPE_CONTACT_LIST)) +#define EMPATHY_IS_CONTACT_LIST_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), EMPATHY_TYPE_CONTACT_LIST)) +#define EMPATHY_CONTACT_LIST_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), EMPATHY_TYPE_CONTACT_LIST, EmpathyContactListClass)) + +typedef struct _EmpathyContactList EmpathyContactList; +typedef struct _EmpathyContactListClass EmpathyContactListClass; +typedef struct _EmpathyContactListPriv EmpathyContactListPriv; + +struct _EmpathyContactList { + GObject parent; +}; + +struct _EmpathyContactListClass { + GObjectClass parent_class; +}; + +GType empathy_contact_list_get_type (void) G_GNUC_CONST; +EmpathyContactList * empathy_contact_list_new (McAccount *account); +void empathy_contact_list_setup (EmpathyContactList *list); +McAccount * empathy_contact_list_get_account (EmpathyContactList *list); +GossipContact * empathy_contact_list_get_own (EmpathyContactList *list); +GossipContact * empathy_contact_list_find (EmpathyContactList *list, + const gchar *id); +void empathy_contact_list_add (EmpathyContactList *list, + guint handle, + const gchar *message); +void empathy_contact_list_remove (EmpathyContactList *list, + guint handle); +GossipContact * empathy_contact_list_get_from_id (EmpathyContactList *list, + const gchar *id); +GossipContact * empathy_contact_list_get_from_handle (EmpathyContactList *list, + guint handle); +GList * empathy_contact_list_get_from_handles (EmpathyContactList *list, + GArray *handles); +void empathy_contact_list_rename_group (EmpathyContactList *list, + const gchar *old_group, + const gchar *new_group); +GList * empathy_contact_list_get_groups (EmpathyContactList *list); +GList * empathy_contact_list_get_contacts (EmpathyContactList *list); + +G_END_DECLS + +#endif /* __EMPATHY_CONTACT_LIST_H__ */ diff --git a/libempathy/empathy-contact-manager.c b/libempathy/empathy-contact-manager.c new file mode 100644 index 00000000..15fb82ee --- /dev/null +++ b/libempathy/empathy-contact-manager.c @@ -0,0 +1,550 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2007 Xavier Claessens <xclaesse@gmail.com> + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include <config.h> + +#include <string.h> + +#include <libtelepathy/tp-constants.h> + +#include "empathy-contact-manager.h" +#include "empathy-session.h" +#include "gossip-utils.h" +#include "gossip-debug.h" + +#define GET_PRIV(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), \ + EMPATHY_TYPE_CONTACT_MANAGER, EmpathyContactManagerPriv)) + +#define DEBUG_DOMAIN "ContactManager" + +struct _EmpathyContactManagerPriv { + GHashTable *lists; + gboolean setup; +}; + +typedef struct { + const gchar *old_group; + const gchar *new_group; +} ContactManagerRenameGroupData; + +typedef struct { + GossipContact *contact; + const gchar *id; +} ContactManagerFindData; + +static void empathy_contact_manager_class_init (EmpathyContactManagerClass *klass); +static void empathy_contact_manager_init (EmpathyContactManager *manager); +static void contact_manager_finalize (GObject *object); +static void contact_manager_setup_foreach (McAccount *account, + EmpathyContactList *list, + EmpathyContactManager *manager); +static gboolean contact_manager_find_foreach (McAccount *account, + EmpathyContactList *list, + ContactManagerFindData *data); +static void contact_manager_add_account (EmpathyContactManager *manager, + McAccount *account); +static void contact_manager_added_cb (EmpathyContactList *list, + GossipContact *contact, + EmpathyContactManager *manager); +static void contact_manager_removed_cb (EmpathyContactList *list, + GossipContact *contact, + EmpathyContactManager *manager); +static void contact_manager_destroy_cb (EmpathyContactList *list, + EmpathyContactManager *manager); +static void contact_manager_rename_group_foreach (McAccount *account, + EmpathyContactList *list, + ContactManagerRenameGroupData *data); +static void contact_manager_get_groups_foreach (McAccount *account, + EmpathyContactList *list, + GList **all_groups); +static void contact_manager_get_contacts_foreach (McAccount *account, + EmpathyContactList *list, + GList **contacts); +static void contact_manager_status_changed_cb (MissionControl *mc, + TelepathyConnectionStatus status, + McPresence presence, + TelepathyConnectionStatusReason reason, + const gchar *unique_name, + EmpathyContactManager *manager); + +enum { + CONTACT_ADDED, + CONTACT_REMOVED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +G_DEFINE_TYPE (EmpathyContactManager, empathy_contact_manager, G_TYPE_OBJECT); + +static void +empathy_contact_manager_class_init (EmpathyContactManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = contact_manager_finalize; + + signals[CONTACT_ADDED] = + g_signal_new ("contact-added", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, + 1, GOSSIP_TYPE_CONTACT); + + signals[CONTACT_REMOVED] = + g_signal_new ("contact-removed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, + 1, GOSSIP_TYPE_CONTACT); + + g_type_class_add_private (object_class, sizeof (EmpathyContactManagerPriv)); +} + +static void +empathy_contact_manager_init (EmpathyContactManager *manager) +{ + EmpathyContactManagerPriv *priv; + MissionControl *mc; + GSList *accounts, *l; + + priv = GET_PRIV (manager); + + priv->lists = g_hash_table_new_full (gossip_account_hash, + gossip_account_equal, + (GDestroyNotify) g_object_unref, + (GDestroyNotify) g_object_unref); + + mc = empathy_session_get_mission_control (); + + dbus_g_proxy_connect_signal (DBUS_G_PROXY (mc), "AccountStatusChanged", + G_CALLBACK (contact_manager_status_changed_cb), + manager, NULL); + + /* Get ContactList for existing connections */ + accounts = mission_control_get_online_connections (mc, NULL); + for (l = accounts; l; l = l->next) { + McAccount *account; + + account = l->data; + contact_manager_add_account (manager, account); + + g_object_unref (account); + } + g_slist_free (accounts); +} + +static void +contact_manager_finalize (GObject *object) +{ + EmpathyContactManagerPriv *priv; + + priv = GET_PRIV (object); + + g_hash_table_destroy (priv->lists); +} + +EmpathyContactManager * +empathy_contact_manager_new (void) +{ + return g_object_new (EMPATHY_TYPE_CONTACT_MANAGER, NULL); +} + +void +empathy_contact_manager_setup (EmpathyContactManager *manager) +{ + EmpathyContactManagerPriv *priv; + + g_return_if_fail (EMPATHY_IS_CONTACT_MANAGER (manager)); + + priv = GET_PRIV (manager); + + if (priv->setup) { + /* Already done */ + return; + } + + g_hash_table_foreach (priv->lists, + (GHFunc) contact_manager_setup_foreach, + manager); + + priv->setup = TRUE; +} + +EmpathyContactList * +empathy_contact_manager_get_list (EmpathyContactManager *manager, + McAccount *account) +{ + EmpathyContactManagerPriv *priv; + + g_return_val_if_fail (EMPATHY_IS_CONTACT_MANAGER (manager), NULL); + g_return_val_if_fail (MC_IS_ACCOUNT (account), NULL); + + priv = GET_PRIV (manager); + + return g_hash_table_lookup (priv->lists, account); +} + +GossipContact * +empathy_contact_manager_get_own (EmpathyContactManager *manager, + McAccount *account) +{ + EmpathyContactManagerPriv *priv; + EmpathyContactList *list; + + g_return_val_if_fail (EMPATHY_IS_CONTACT_MANAGER (manager), NULL); + g_return_val_if_fail (MC_IS_ACCOUNT (account), NULL); + + priv = GET_PRIV (manager); + + list = g_hash_table_lookup (priv->lists, account); + + if (!list) { + return NULL; + } + + return empathy_contact_list_get_own (list); +} + +GossipContact * +empathy_contact_manager_create (EmpathyContactManager *manager, + McAccount *account, + const gchar *id) +{ + EmpathyContactManagerPriv *priv; + EmpathyContactList *list; + + g_return_val_if_fail (EMPATHY_IS_CONTACT_MANAGER (manager), NULL); + g_return_val_if_fail (MC_IS_ACCOUNT (account), NULL); + g_return_val_if_fail (id != NULL, NULL); + + priv = GET_PRIV (manager); + + list = g_hash_table_lookup (priv->lists, account); + + if (!list) { + return NULL; + } + + return empathy_contact_list_get_from_id (list, id); +} + +GossipContact * +empathy_contact_manager_find (EmpathyContactManager *manager, + const gchar *id) +{ + EmpathyContactManagerPriv *priv; + ContactManagerFindData data; + + g_return_val_if_fail (EMPATHY_IS_CONTACT_MANAGER (manager), NULL); + g_return_val_if_fail (id != NULL, NULL); + + priv = GET_PRIV (manager); + + data.contact = NULL; + data.id = id; + + g_hash_table_find (priv->lists, + (GHRFunc) contact_manager_find_foreach, + &data); + + return data.contact; +} + +void +empathy_contact_manager_add (EmpathyContactManager *manager, + GossipContact *contact, + const gchar *message) +{ + EmpathyContactManagerPriv *priv; + EmpathyContactList *list; + McAccount *account; + guint handle; + + g_return_if_fail (EMPATHY_IS_CONTACT_MANAGER (manager)); + g_return_if_fail (GOSSIP_IS_CONTACT (contact)); + + priv = GET_PRIV (manager); + + account = gossip_contact_get_account (contact); + handle = gossip_contact_get_handle (contact); + list = g_hash_table_lookup (priv->lists, account); + + if (list) { + empathy_contact_list_add (list, handle, message); + } +} + +void +empathy_contact_manager_remove (EmpathyContactManager *manager, + GossipContact *contact) +{ + EmpathyContactManagerPriv *priv; + EmpathyContactList *list; + McAccount *account; + guint handle; + + g_return_if_fail (EMPATHY_IS_CONTACT_MANAGER (manager)); + g_return_if_fail (GOSSIP_IS_CONTACT (contact)); + + priv = GET_PRIV (manager); + + account = gossip_contact_get_account (contact); + handle = gossip_contact_get_handle (contact); + list = g_hash_table_lookup (priv->lists, account); + + if (list) { + empathy_contact_list_remove (list, handle); + } +} + +void +empathy_contact_manager_rename_group (EmpathyContactManager *manager, + const gchar *old_group, + const gchar *new_group) +{ + EmpathyContactManagerPriv *priv; + ContactManagerRenameGroupData data; + + g_return_if_fail (EMPATHY_IS_CONTACT_MANAGER (manager)); + g_return_if_fail (old_group != NULL); + g_return_if_fail (new_group != NULL); + + priv = GET_PRIV (manager); + + data.old_group = old_group; + data.new_group = new_group; + + g_hash_table_foreach (priv->lists, + (GHFunc) contact_manager_rename_group_foreach, + &data); +} + +GList * +empathy_contact_manager_get_groups (EmpathyContactManager *manager) +{ + EmpathyContactManagerPriv *priv; + GList *groups = NULL; + + g_return_val_if_fail (EMPATHY_IS_CONTACT_MANAGER (manager), NULL); + + priv = GET_PRIV (manager); + + g_hash_table_foreach (priv->lists, + (GHFunc) contact_manager_get_groups_foreach, + &groups); + + return groups; +} + +GList * +empathy_contact_manager_get_contacts (EmpathyContactManager *manager) +{ + EmpathyContactManagerPriv *priv; + GList *contacts = NULL; + + g_return_val_if_fail (EMPATHY_IS_CONTACT_MANAGER (manager), NULL); + + priv = GET_PRIV (manager); + + g_hash_table_foreach (priv->lists, + (GHFunc) contact_manager_get_contacts_foreach, + &contacts); + + return contacts; +} + +static void +contact_manager_setup_foreach (McAccount *account, + EmpathyContactList *list, + EmpathyContactManager *manager) +{ + empathy_contact_list_setup (list); +} + +static gboolean +contact_manager_find_foreach (McAccount *account, + EmpathyContactList *list, + ContactManagerFindData *data) +{ + data->contact = empathy_contact_list_find (list, data->id); + + if (data->contact) { + return TRUE; + } + + return FALSE; +} + +static void +contact_manager_add_account (EmpathyContactManager *manager, + McAccount *account) +{ + EmpathyContactManagerPriv *priv; + EmpathyContactList *list; + + priv = GET_PRIV (manager); + + if (g_hash_table_lookup (priv->lists, account)) { + return; + } + + gossip_debug (DEBUG_DOMAIN, "Adding new account: %s", + mc_account_get_display_name (account)); + + list = empathy_contact_list_new (account); + if (!list) { + return; + } + + g_hash_table_insert (priv->lists, g_object_ref (account), list); + + /* Connect signals */ + g_signal_connect (list, "contact-added", + G_CALLBACK (contact_manager_added_cb), + manager); + g_signal_connect (list, "contact-removed", + G_CALLBACK (contact_manager_removed_cb), + manager); + g_signal_connect (list, "destroy", + G_CALLBACK (contact_manager_destroy_cb), + manager); + + if (priv->setup) { + empathy_contact_list_setup (list); + } +} + +static void +contact_manager_added_cb (EmpathyContactList *list, + GossipContact *contact, + EmpathyContactManager *manager) +{ + g_signal_emit (manager, signals[CONTACT_ADDED], 0, contact); +} + +static void +contact_manager_removed_cb (EmpathyContactList *list, + GossipContact *contact, + EmpathyContactManager *manager) +{ + g_signal_emit (manager, signals[CONTACT_REMOVED], 0, contact); +} + +static void +contact_manager_destroy_cb (EmpathyContactList *list, + EmpathyContactManager *manager) +{ + EmpathyContactManagerPriv *priv; + McAccount *account; + + priv = GET_PRIV (manager); + + account = empathy_contact_list_get_account (list); + + gossip_debug (DEBUG_DOMAIN, "Removing account: %s", + mc_account_get_display_name (account)); + + /* Disconnect signals from the list */ + g_signal_handlers_disconnect_by_func (list, + contact_manager_added_cb, + manager); + g_signal_handlers_disconnect_by_func (list, + contact_manager_removed_cb, + manager); + g_signal_handlers_disconnect_by_func (list, + contact_manager_destroy_cb, + manager); + + g_hash_table_remove (priv->lists, account); +} + +static void +contact_manager_rename_group_foreach (McAccount *account, + EmpathyContactList *list, + ContactManagerRenameGroupData *data) +{ + empathy_contact_list_rename_group (list, + data->old_group, + data->new_group); +} + +static void +contact_manager_get_groups_foreach (McAccount *account, + EmpathyContactList *list, + GList **all_groups) +{ + GList *groups, *l; + + groups = empathy_contact_list_get_groups (list); + for (l = groups; l; l = l->next) { + if (!g_list_find_custom (*all_groups, + l->data, + (GCompareFunc) strcmp)) { + *all_groups = g_list_append (*all_groups, + g_strdup (l->data)); + } + g_free (l->data); + } + + g_list_free (groups); +} + +static void +contact_manager_get_contacts_foreach (McAccount *account, + EmpathyContactList *list, + GList **contacts) +{ + GList *l; + + l = empathy_contact_list_get_contacts (list); + *contacts = g_list_concat (*contacts, l); +} + +static void +contact_manager_status_changed_cb (MissionControl *mc, + TelepathyConnectionStatus status, + McPresence presence, + TelepathyConnectionStatusReason reason, + const gchar *unique_name, + EmpathyContactManager *manager) +{ + EmpathyContactManagerPriv *priv; + McAccount *account; + + priv = GET_PRIV (manager); + + if (status != TP_CONN_STATUS_CONNECTED) { + /* We only care about newly connected accounts */ + return; + } + + account = mc_account_lookup (unique_name); + contact_manager_add_account (manager, account); + + g_object_unref (account); +} + diff --git a/libempathy/empathy-contact-manager.h b/libempathy/empathy-contact-manager.h new file mode 100644 index 00000000..ca6cb3fe --- /dev/null +++ b/libempathy/empathy-contact-manager.h @@ -0,0 +1,77 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2007 Xavier Claessens <xclaesse@gmail.com> + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __EMPATHY_CONTACT_MANAGER_H__ +#define __EMPATHY_CONTACT_MANAGER_H__ + +#include <glib.h> + +#include <libmissioncontrol/mc-account.h> + +#include "gossip-contact.h" +#include "empathy-contact-list.h" + +G_BEGIN_DECLS + +#define EMPATHY_TYPE_CONTACT_MANAGER (empathy_contact_manager_get_type ()) +#define EMPATHY_CONTACT_MANAGER(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), EMPATHY_TYPE_CONTACT_MANAGER, EmpathyContactManager)) +#define EMPATHY_CONTACT_MANAGER_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), EMPATHY_TYPE_CONTACT_MANAGER, EmpathyContactManagerClass)) +#define EMPATHY_IS_CONTACT_MANAGER(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), EMPATHY_TYPE_CONTACT_MANAGER)) +#define EMPATHY_IS_CONTACT_MANAGER_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), EMPATHY_TYPE_CONTACT_MANAGER)) +#define EMPATHY_CONTACT_MANAGER_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), EMPATHY_TYPE_CONTACT_MANAGER, EmpathyContactManagerClass)) + +typedef struct _EmpathyContactManager EmpathyContactManager; +typedef struct _EmpathyContactManagerClass EmpathyContactManagerClass; +typedef struct _EmpathyContactManagerPriv EmpathyContactManagerPriv; + +struct _EmpathyContactManager { + GObject parent; +}; + +struct _EmpathyContactManagerClass { + GObjectClass parent_class; +}; + +GType empathy_contact_manager_get_type (void) G_GNUC_CONST; +EmpathyContactManager *empathy_contact_manager_new (void); +void empathy_contact_manager_setup (EmpathyContactManager *manager); +EmpathyContactList * empathy_contact_manager_get_list (EmpathyContactManager *manager, + McAccount *account); +GossipContact * empathy_contact_manager_get_own (EmpathyContactManager *manager, + McAccount *account); +GossipContact * empathy_contact_manager_find (EmpathyContactManager *manager, + const gchar *id); +GossipContact * empathy_contact_manager_create (EmpathyContactManager *manager, + McAccount *account, + const gchar *id); +void empathy_contact_manager_add (EmpathyContactManager *manager, + GossipContact *contact, + const gchar *message); +void empathy_contact_manager_remove (EmpathyContactManager *manager, + GossipContact *contact); +void empathy_contact_manager_rename_group (EmpathyContactManager *manager, + const gchar *old_group, + const gchar *new_group); +GList * empathy_contact_manager_get_groups (EmpathyContactManager *manager); +GList * empathy_contact_manager_get_contacts (EmpathyContactManager *manager); + +G_END_DECLS + +#endif /* __EMPATHY_CONTACT_MANAGER_H__ */ diff --git a/libempathy/empathy-marshal-main.c b/libempathy/empathy-marshal-main.c new file mode 100644 index 00000000..f3ab95f5 --- /dev/null +++ b/libempathy/empathy-marshal-main.c @@ -0,0 +1,2 @@ +#include "empathy-marshal.h" +#include "empathy-marshal.c" diff --git a/libempathy/empathy-marshal.list b/libempathy/empathy-marshal.list new file mode 100644 index 00000000..6035b945 --- /dev/null +++ b/libempathy/empathy-marshal.list @@ -0,0 +1,3 @@ +VOID:POINTER,UINT,UINT,STRING +VOID:OBJECT,UINT +VOID:OBJECT,OBJECT diff --git a/libempathy/empathy-session.c b/libempathy/empathy-session.c new file mode 100644 index 00000000..fefda769 --- /dev/null +++ b/libempathy/empathy-session.c @@ -0,0 +1,161 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2007 Xavier Claessens <xclaesse@gmail.com> + * + * This library 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 library 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 library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include <config.h> + +#include <glib.h> + +#include <libtelepathy/tp-helpers.h> + +#include <libmissioncontrol/mc-account-monitor.h> + +#include "empathy-session.h" +#include "gossip-debug.h" + +#define DEBUG_DOMAIN "Session" + +static void session_start_mission_control (void); +static void session_error_cb (MissionControl *mc, + GError *error, + gpointer data); +static void session_account_enabled_cb (McAccountMonitor *monitor, + gchar *unique_name, + gpointer user_data); +static void session_service_ended_cb (MissionControl *mc, + gpointer user_data); + +static MissionControl *mission_control = NULL; +static EmpathyContactManager *contact_manager = NULL; + +void +empathy_session_connect (void) +{ + MissionControl *mc; + McAccountMonitor *monitor; + static gboolean started = FALSE; + + if (started) { + return; + } + + mc = empathy_session_get_mission_control (); + monitor = mc_account_monitor_new (); + + g_signal_connect (monitor, "account-enabled", + G_CALLBACK (session_account_enabled_cb), + NULL); + g_signal_connect (mc, "ServiceEnded", + G_CALLBACK (session_service_ended_cb), + NULL); + + g_object_unref (monitor); + session_start_mission_control (); + + started = TRUE; +} + +void +empathy_session_finalize (void) +{ + if (mission_control) { + g_object_unref (mission_control); + mission_control = NULL; + } + + if (contact_manager) { + g_object_unref (contact_manager); + contact_manager = NULL; + } +} + +MissionControl * +empathy_session_get_mission_control (void) +{ + if (!mission_control) { + mission_control = mission_control_new (tp_get_bus ()); + } + + return mission_control; +} + +EmpathyContactManager * +empathy_session_get_contact_manager (void) +{ + if (!contact_manager) { + contact_manager = empathy_contact_manager_new (); + } + + return contact_manager; +} + +static void +session_start_mission_control (void) +{ + MissionControl *mc; + McPresence presence; + + mc = empathy_session_get_mission_control (); + presence = mission_control_get_presence_actual (mc, NULL); + + if (presence != MC_PRESENCE_UNSET && + presence != MC_PRESENCE_OFFLINE) { + /* MC is already running and online, nothing to do */ + return; + } + + gossip_debug (DEBUG_DOMAIN, "Starting Mission Control..."); + + /* FIXME: Save/Restore status message */ + mission_control_set_presence (mc, MC_PRESENCE_AVAILABLE, + NULL, + (McCallback) session_error_cb, + NULL); + + mission_control_connect_all_with_default_presence (mc, + (McCallback) session_error_cb, + NULL); +} + +static void +session_error_cb (MissionControl *mc, + GError *error, + gpointer data) +{ + if (error) { + gossip_debug (DEBUG_DOMAIN, "Error: %s", error->message); + } +} + +static void +session_account_enabled_cb (McAccountMonitor *monitor, + gchar *unique_name, + gpointer user_data) +{ + gossip_debug (DEBUG_DOMAIN, "Account enabled: %s", unique_name); + session_start_mission_control (); +} + +static void +session_service_ended_cb (MissionControl *mc, + gpointer user_data) +{ + gossip_debug (DEBUG_DOMAIN, "Mission Control stopped"); +} + diff --git a/libempathy/empathy-session.h b/libempathy/empathy-session.h new file mode 100644 index 00000000..0d3a2dce --- /dev/null +++ b/libempathy/empathy-session.h @@ -0,0 +1,38 @@ +#include <libtelepathy/tp-helpers.h>/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2007 Xavier Claessens <xclaesse@gmail.com> + * + * This library 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 library 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 library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __EMPATHY_SESSION_H__ +#define __EMPATHY_SESSION_H__ + +#include <glib.h> + +#include <libmissioncontrol/mission-control.h> +#include "empathy-contact-manager.h" + +G_BEGIN_DECLS + +void empathy_session_connect (void); +void empathy_session_finalize (void); +MissionControl * empathy_session_get_mission_control (void); +EmpathyContactManager *empathy_session_get_contact_manager (void); + +G_END_DECLS + +#endif /* __EMPATHY_MISSION_CONTROL_H__ */ diff --git a/libempathy/empathy-tp-chat.c b/libempathy/empathy-tp-chat.c new file mode 100644 index 00000000..ad00711d --- /dev/null +++ b/libempathy/empathy-tp-chat.c @@ -0,0 +1,474 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2007 Xavier Claessens <xclaesse@gmail.com> + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include <config.h> + +#include <libtelepathy/tp-helpers.h> +#include <libtelepathy/tp-chan-type-text-gen.h> +#include <libtelepathy/tp-chan-iface-chat-state-gen.h> + +#include "empathy-tp-chat.h" +#include "empathy-contact-manager.h" +#include "empathy-contact-list.h" +#include "empathy-session.h" +#include "empathy-marshal.h" +#include "gossip-debug.h" +#include "gossip-time.h" + +#define GET_PRIV(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), \ + EMPATHY_TYPE_TP_CHAT, EmpathyTpChatPriv)) + +#define DEBUG_DOMAIN "TpChat" + +struct _EmpathyTpChatPriv { + EmpathyContactList *list; + TpChan *tp_chan; + DBusGProxy *text_iface; + DBusGProxy *chat_state_iface; +}; + +static void empathy_tp_chat_class_init (EmpathyTpChatClass *klass); +static void empathy_tp_chat_init (EmpathyTpChat *chat); +static void tp_chat_finalize (GObject *object); +static void tp_chat_destroy_cb (TpChan *text_chan, + EmpathyTpChat *chat); +static void tp_chat_received_cb (DBusGProxy *text_iface, + guint message_id, + guint timestamp, + guint from_handle, + guint message_type, + guint message_flags, + gchar *message_body, + EmpathyTpChat *chat); +static void tp_chat_sent_cb (DBusGProxy *text_iface, + guint timestamp, + guint message_type, + gchar *message_body, + EmpathyTpChat *chat); +static void tp_chat_state_changed_cb (DBusGProxy *chat_state_iface, + guint handle, + EmpathyTpChatState state, + EmpathyTpChat *chat); +static void tp_chat_emit_message (EmpathyTpChat *chat, + guint type, + guint timestamp, + guint from_handle, + const gchar *message_body); + +enum { + MESSAGE_RECEIVED, + CHAT_STATE_CHANGED, + DESTROY, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +G_DEFINE_TYPE (EmpathyTpChat, empathy_tp_chat, G_TYPE_OBJECT); + +static void +empathy_tp_chat_class_init (EmpathyTpChatClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = tp_chat_finalize; + + signals[MESSAGE_RECEIVED] = + g_signal_new ("message-received", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, + 1, GOSSIP_TYPE_MESSAGE); + + signals[CHAT_STATE_CHANGED] = + g_signal_new ("chat-state-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + empathy_marshal_VOID__OBJECT_UINT, + G_TYPE_NONE, + 2, GOSSIP_TYPE_CONTACT, G_TYPE_UINT); + + signals[DESTROY] = + g_signal_new ("destroy", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); + + g_type_class_add_private (object_class, sizeof (EmpathyTpChatPriv)); +} + +static void +empathy_tp_chat_init (EmpathyTpChat *chat) +{ +} + + +static void +tp_chat_finalize (GObject *object) +{ + EmpathyTpChatPriv *priv; + EmpathyTpChat *chat; + GError *error = NULL; + + chat = EMPATHY_TP_CHAT (object); + priv = GET_PRIV (chat); + + if (priv->tp_chan) { + gossip_debug (DEBUG_DOMAIN, "Closing channel..."); + + g_signal_handlers_disconnect_by_func (priv->tp_chan, + tp_chat_destroy_cb, + object); + + if (!tp_chan_close (DBUS_G_PROXY (priv->tp_chan), &error)) { + gossip_debug (DEBUG_DOMAIN, + "Error closing text channel: %s", + error ? error->message : "No error given"); + g_clear_error (&error); + } + g_object_unref (priv->tp_chan); + } + + if (priv->list) { + g_object_unref (priv->list); + } + + G_OBJECT_CLASS (empathy_tp_chat_parent_class)->finalize (object); +} + +EmpathyTpChat * +empathy_tp_chat_new (McAccount *account, + TpChan *tp_chan) +{ + EmpathyTpChatPriv *priv; + EmpathyTpChat *chat; + EmpathyContactManager *manager; + + g_return_val_if_fail (MC_IS_ACCOUNT (account), NULL); + g_return_val_if_fail (TELEPATHY_IS_CHAN (tp_chan), NULL); + + chat = g_object_new (EMPATHY_TYPE_TP_CHAT, NULL); + priv = GET_PRIV (chat); + + manager = empathy_session_get_contact_manager (); + priv->list = empathy_contact_manager_get_list (manager, account); + priv->tp_chan = g_object_ref (tp_chan); + g_object_ref (priv->list); + + priv->text_iface = tp_chan_get_interface (tp_chan, + TELEPATHY_CHAN_IFACE_TEXT_QUARK); + priv->chat_state_iface = tp_chan_get_interface (tp_chan, + TELEPATHY_CHAN_IFACE_CHAT_STATE_QUARK); + + g_signal_connect (priv->tp_chan, "destroy", + G_CALLBACK (tp_chat_destroy_cb), + chat); + dbus_g_proxy_connect_signal (priv->text_iface, "Received", + G_CALLBACK (tp_chat_received_cb), + chat, NULL); + dbus_g_proxy_connect_signal (priv->text_iface, "Sent", + G_CALLBACK (tp_chat_sent_cb), + chat, NULL); + + if (priv->chat_state_iface != NULL) { + dbus_g_proxy_connect_signal (priv->chat_state_iface, + "ChatStateChanged", + G_CALLBACK (tp_chat_state_changed_cb), + chat, NULL); + } + + return chat; +} + +EmpathyTpChat * +empathy_tp_chat_new_with_contact (GossipContact *contact) +{ + EmpathyTpChat *chat; + MissionControl *mc; + McAccount *account; + TpConn *tp_conn; + TpChan *text_chan; + const gchar *bus_name; + guint handle; + + g_return_val_if_fail (GOSSIP_IS_CONTACT (contact), NULL); + + mc = empathy_session_get_mission_control (); + account = gossip_contact_get_account (contact); + + if (mission_control_get_connection_status (mc, account, NULL) != 0) { + /* The account is not connected, nothing to do. */ + return NULL; + } + + tp_conn = mission_control_get_connection (mc, account, NULL); + g_return_val_if_fail (tp_conn != NULL, NULL); + bus_name = dbus_g_proxy_get_bus_name (DBUS_G_PROXY (tp_conn)); + handle = gossip_contact_get_handle (contact); + + text_chan = tp_conn_new_channel (tp_get_bus (), + tp_conn, + bus_name, + TP_IFACE_CHANNEL_TYPE_TEXT, + TP_HANDLE_TYPE_CONTACT, + handle, + TRUE); + + chat = empathy_tp_chat_new (account, text_chan); + + g_object_unref (tp_conn); + g_object_unref (text_chan); + + return chat; +} + +void +empathy_tp_chat_request_pending (EmpathyTpChat *chat) +{ + EmpathyTpChatPriv *priv; + GPtrArray *messages_list; + guint i; + GError *error = NULL; + + g_return_if_fail (EMPATHY_IS_TP_CHAT (chat)); + + priv = GET_PRIV (chat); + + /* If we do this call async, don't forget to ignore Received signal + * until we get the answer */ + if (!tp_chan_type_text_list_pending_messages (priv->text_iface, + TRUE, + &messages_list, + &error)) { + gossip_debug (DEBUG_DOMAIN, + "Error retrieving pending messages: %s", + error ? error->message : "No error given"); + g_clear_error (&error); + return; + } + + for (i = 0; i < messages_list->len; i++) { + GValueArray *message_struct; + const gchar *message_body; + guint message_id; + guint timestamp; + guint from_handle; + guint message_type; + guint message_flags; + + message_struct = g_ptr_array_index (messages_list, i); + + message_id = g_value_get_uint (g_value_array_get_nth (message_struct, 0)); + timestamp = g_value_get_uint (g_value_array_get_nth (message_struct, 1)); + from_handle = g_value_get_uint (g_value_array_get_nth (message_struct, 2)); + message_type = g_value_get_uint (g_value_array_get_nth (message_struct, 3)); + message_flags = g_value_get_uint (g_value_array_get_nth (message_struct, 4)); + message_body = g_value_get_string (g_value_array_get_nth (message_struct, 5)); + + gossip_debug (DEBUG_DOMAIN, "Message pending: %s", message_body); + + tp_chat_emit_message (chat, + message_type, + timestamp, + from_handle, + message_body); + + g_value_array_free (message_struct); + } + + g_ptr_array_free (messages_list, TRUE); +} + +void +empathy_tp_chat_send (EmpathyTpChat *chat, + GossipMessage *message) +{ + EmpathyTpChatPriv *priv; + const gchar *message_body; + GossipMessageType message_type; + GError *error = NULL; + + g_return_if_fail (EMPATHY_IS_TP_CHAT (chat)); + g_return_if_fail (GOSSIP_IS_MESSAGE (message)); + + priv = GET_PRIV (chat); + + message_body = gossip_message_get_body (message); + message_type = gossip_message_get_type (message); + + gossip_debug (DEBUG_DOMAIN, "Sending message: %s", message_body); + if (!tp_chan_type_text_send (priv->text_iface, + message_type, + message_body, + &error)) { + gossip_debug (DEBUG_DOMAIN, + "Send Error: %s", + error ? error->message : "No error given"); + g_clear_error (&error); + } +} + +void +empathy_tp_chat_send_state (EmpathyTpChat *chat, + EmpathyTpChatState state) +{ + EmpathyTpChatPriv *priv; + GError *error = NULL; + + g_return_if_fail (EMPATHY_IS_TP_CHAT (chat)); + + priv = GET_PRIV (chat); + + if (priv->chat_state_iface) { + gossip_debug (DEBUG_DOMAIN, "Set state: %d", state); + if (!tp_chan_iface_chat_state_set_chat_state (priv->chat_state_iface, + state, + &error)) { + gossip_debug (DEBUG_DOMAIN, + "Set Chat State Error: %s", + error ? error->message : "No error given"); + g_clear_error (&error); + } + } +} + +static void +tp_chat_destroy_cb (TpChan *text_chan, + EmpathyTpChat *chat) +{ + EmpathyTpChatPriv *priv; + + priv = GET_PRIV (chat); + + gossip_debug (DEBUG_DOMAIN, "Channel destroyed"); + + g_object_unref (priv->tp_chan); + priv->tp_chan = NULL; + priv->text_iface = NULL; + priv->chat_state_iface = NULL; + + g_signal_emit (chat, signals[DESTROY], 0); +} + +static void +tp_chat_received_cb (DBusGProxy *text_iface, + guint message_id, + guint timestamp, + guint from_handle, + guint message_type, + guint message_flags, + gchar *message_body, + EmpathyTpChat *chat) +{ + EmpathyTpChatPriv *priv; + GArray *message_ids; + + priv = GET_PRIV (chat); + + gossip_debug (DEBUG_DOMAIN, "Message received: %s", message_body); + + tp_chat_emit_message (chat, + message_type, + timestamp, + from_handle, + message_body); + + message_ids = g_array_new (FALSE, FALSE, sizeof (guint)); + g_array_append_val (message_ids, message_id); + tp_chan_type_text_acknowledge_pending_messages (priv->text_iface, + message_ids, NULL); + g_array_free (message_ids, TRUE); +} + +static void +tp_chat_sent_cb (DBusGProxy *text_iface, + guint timestamp, + guint message_type, + gchar *message_body, + EmpathyTpChat *chat) +{ + gossip_debug (DEBUG_DOMAIN, "Message sent: %s", message_body); + + tp_chat_emit_message (chat, + message_type, + timestamp, + 0, + message_body); +} + +static void +tp_chat_state_changed_cb (DBusGProxy *chat_state_iface, + guint handle, + EmpathyTpChatState state, + EmpathyTpChat *chat) +{ + EmpathyTpChatPriv *priv; + GossipContact *contact; + + priv = GET_PRIV (chat); + + contact = empathy_contact_list_get_from_handle (priv->list, handle); + + g_signal_emit (chat, signals[CHAT_STATE_CHANGED], 0, contact, state); + + g_object_unref (contact); +} + +static void +tp_chat_emit_message (EmpathyTpChat *chat, + guint type, + guint timestamp, + guint from_handle, + const gchar *message_body) +{ + EmpathyTpChatPriv *priv; + GossipMessage *message; + GossipContact *sender; + + priv = GET_PRIV (chat); + + if (from_handle == 0) { + sender = empathy_contact_list_get_own (priv->list); + g_object_ref (sender); + } else { + sender = empathy_contact_list_get_from_handle (priv->list, + from_handle); + } + + message = gossip_message_new (message_body); + gossip_message_set_type (message, type); + gossip_message_set_sender (message, sender); + gossip_message_set_timestamp (message, (GossipTime) timestamp); + + g_signal_emit (chat, signals[MESSAGE_RECEIVED], 0, message); + + g_object_unref (message); + g_object_unref (sender); +} + diff --git a/libempathy/empathy-tp-chat.h b/libempathy/empathy-tp-chat.h new file mode 100644 index 00000000..c64bbd73 --- /dev/null +++ b/libempathy/empathy-tp-chat.h @@ -0,0 +1,75 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2007 Xavier Claessens <xclaesse@gmail.com> + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __EMPATHY_TP_CHAT_H__ +#define __EMPATHY_TP_CHAT_H__ + +#include <glib.h> + +#include <libtelepathy/tp-chan.h> + +#include <libmissioncontrol/mc-account.h> + +#include "gossip-message.h" +#include "gossip-contact.h" + + +G_BEGIN_DECLS + +#define EMPATHY_TYPE_TP_CHAT (empathy_tp_chat_get_type ()) +#define EMPATHY_TP_CHAT(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), EMPATHY_TYPE_TP_CHAT, EmpathyTpChat)) +#define EMPATHY_TP_CHAT_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), EMPATHY_TYPE_TP_CHAT, EmpathyTpChatClass)) +#define EMPATHY_IS_TP_CHAT(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), EMPATHY_TYPE_TP_CHAT)) +#define EMPATHY_IS_TP_CHAT_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), EMPATHY_TYPE_TP_CHAT)) +#define EMPATHY_TP_CHAT_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), EMPATHY_TYPE_TP_CHAT, EmpathyTpChatClass)) + +typedef struct _EmpathyTpChat EmpathyTpChat; +typedef struct _EmpathyTpChatClass EmpathyTpChatClass; +typedef struct _EmpathyTpChatPriv EmpathyTpChatPriv; + +struct _EmpathyTpChat { + GObject parent; +}; + +struct _EmpathyTpChatClass { + GObjectClass parent_class; +}; + +typedef enum { + EMPATHY_TP_CHAT_STATE_GONE, + EMPATHY_TP_CHAT_STATE_INACTIVE, + EMPATHY_TP_CHAT_STATE_ACTIVE, + EMPATHY_TP_CHAT_STATE_PAUSED, + EMPATHY_TP_CHAT_STATE_COMPOSING +} EmpathyTpChatState; + +GType empathy_tp_chat_get_type (void) G_GNUC_CONST; +EmpathyTpChat *empathy_tp_chat_new (McAccount *account, + TpChan *tp_chan); +EmpathyTpChat *empathy_tp_chat_new_with_contact (GossipContact *contact); +void empathy_tp_chat_request_pending (EmpathyTpChat *chat); +void empathy_tp_chat_send (EmpathyTpChat *chat, + GossipMessage *message); +void empathy_tp_chat_send_state (EmpathyTpChat *chat, + EmpathyTpChatState state); + +G_END_DECLS + +#endif /* __EMPATHY_TP_CHAT_H__ */ diff --git a/libempathy/gossip-avatar.c b/libempathy/gossip-avatar.c new file mode 100644 index 00000000..5c17a517 --- /dev/null +++ b/libempathy/gossip-avatar.c @@ -0,0 +1,86 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2002-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Martyn Russell <martyn@imendio.com> + * Xavier Claessens <xclaesse@gmail.com> + */ + +#include "config.h" + +#include "gossip-avatar.h" + +#define DEBUG_DOMAIN "Avatar" + +GType +gossip_avatar_get_gtype (void) +{ + static GType type_id = 0; + + if (!type_id) { + type_id = g_boxed_type_register_static ("GossipAvatar", + (GBoxedCopyFunc) gossip_avatar_ref, + (GBoxedFreeFunc) gossip_avatar_unref); + } + + return type_id; +} + +GossipAvatar * +gossip_avatar_new (guchar *data, + gsize len, + gchar *format) +{ + GossipAvatar *avatar; + + g_return_val_if_fail (data != NULL, NULL); + g_return_val_if_fail (len > 0, NULL); + g_return_val_if_fail (format != NULL, NULL); + + avatar = g_slice_new0 (GossipAvatar); + avatar->data = g_memdup (data, len); + avatar->len = len; + avatar->format = g_strdup (format); + avatar->refcount = 1; + + return avatar; +} + +void +gossip_avatar_unref (GossipAvatar *avatar) +{ + g_return_if_fail (avatar != NULL); + + avatar->refcount--; + if (avatar->refcount == 0) { + g_free (avatar->data); + g_free (avatar->format); + g_slice_free (GossipAvatar, avatar); + } +} + +GossipAvatar * +gossip_avatar_ref (GossipAvatar *avatar) +{ + g_return_val_if_fail (avatar != NULL, NULL); + + avatar->refcount++; + + return avatar; +} + diff --git a/libempathy/gossip-avatar.h b/libempathy/gossip-avatar.h new file mode 100644 index 00000000..44fa9aba --- /dev/null +++ b/libempathy/gossip-avatar.h @@ -0,0 +1,48 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2006 Xavier Claessens <xclaesse@gmail.com> + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __GOSSIP_AVATAR_H__ +#define __GOSSIP_AVATAR_H__ + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GOSSIP_TYPE_AVATAR (gossip_avatar_get_gtype ()) + +typedef struct _GossipAvatar GossipAvatar; + +struct _GossipAvatar { + guchar *data; + gsize len; + gchar *format; + guint refcount; +}; + +GType gossip_avatar_get_gtype (void) G_GNUC_CONST; +GossipAvatar * gossip_avatar_new (guchar *avatar, + gsize len, + gchar *format); +GossipAvatar * gossip_avatar_ref (GossipAvatar *avatar); +void gossip_avatar_unref (GossipAvatar *avatar); + +G_END_DECLS + +#endif /* __GOSSIP_AVATAR_H__ */ diff --git a/libempathy/gossip-conf.c b/libempathy/gossip-conf.c new file mode 100644 index 00000000..9625a700 --- /dev/null +++ b/libempathy/gossip-conf.c @@ -0,0 +1,382 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2006 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Richard Hult <richard@imendio.com> + */ + +#include "config.h" + +#include <string.h> + +#include <gconf/gconf-client.h> + +#include "gossip-conf.h" +#include "gossip-debug.h" + +#define DEBUG_DOMAIN "Config" + +#define GOSSIP_CONF_ROOT "/apps/empathy" +#define DESKTOP_INTERFACE_ROOT "/desktop/gnome/interface" + +#define GET_PRIV(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GOSSIP_TYPE_CONF, GossipConfPriv)) + +typedef struct { + GConfClient *gconf_client; +} GossipConfPriv; + +typedef struct { + GossipConf *conf; + GossipConfNotifyFunc func; + gpointer user_data; +} GossipConfNotifyData; + +static void conf_finalize (GObject *object); + +G_DEFINE_TYPE (GossipConf, gossip_conf, G_TYPE_OBJECT); + +static GossipConf *global_conf = NULL; + +static void +gossip_conf_class_init (GossipConfClass *class) +{ + GObjectClass *object_class; + + object_class = G_OBJECT_CLASS (class); + + object_class->finalize = conf_finalize; + + g_type_class_add_private (object_class, sizeof (GossipConfPriv)); +} + +static void +gossip_conf_init (GossipConf *conf) +{ + GossipConfPriv *priv; + + priv = GET_PRIV (conf); + + priv->gconf_client = gconf_client_get_default (); + + gconf_client_add_dir (priv->gconf_client, + GOSSIP_CONF_ROOT, + GCONF_CLIENT_PRELOAD_ONELEVEL, + NULL); + gconf_client_add_dir (priv->gconf_client, + DESKTOP_INTERFACE_ROOT, + GCONF_CLIENT_PRELOAD_NONE, + NULL); +} + +static void +conf_finalize (GObject *object) +{ + GossipConfPriv *priv; + + priv = GET_PRIV (object); + + gconf_client_remove_dir (priv->gconf_client, + GOSSIP_CONF_ROOT, + NULL); + gconf_client_remove_dir (priv->gconf_client, + DESKTOP_INTERFACE_ROOT, + NULL); + + g_object_unref (priv->gconf_client); + + G_OBJECT_CLASS (gossip_conf_parent_class)->finalize (object); +} + +GossipConf * +gossip_conf_get (void) +{ + if (!global_conf) { + global_conf = g_object_new (GOSSIP_TYPE_CONF, NULL); + } + + return global_conf; +} + +void +gossip_conf_shutdown (void) +{ + if (global_conf) { + g_object_unref (global_conf); + global_conf = NULL; + } +} + +gboolean +gossip_conf_set_int (GossipConf *conf, + const gchar *key, + gint value) +{ + GossipConfPriv *priv; + + g_return_val_if_fail (GOSSIP_IS_CONF (conf), FALSE); + + gossip_debug (DEBUG_DOMAIN, "Setting int:'%s' to %d", key, value); + + priv = GET_PRIV (conf); + + return gconf_client_set_int (priv->gconf_client, + key, + value, + NULL); +} + +gboolean +gossip_conf_get_int (GossipConf *conf, + const gchar *key, + gint *value) +{ + GossipConfPriv *priv; + GError *error = NULL; + + *value = 0; + + g_return_val_if_fail (GOSSIP_IS_CONF (conf), FALSE); + g_return_val_if_fail (value != NULL, FALSE); + + priv = GET_PRIV (conf); + + *value = gconf_client_get_int (priv->gconf_client, + key, + &error); + + gossip_debug (DEBUG_DOMAIN, "Getting int:'%s' (=%d), error:'%s'", + key, *value, error ? error->message : "None"); + + if (error) { + g_error_free (error); + return FALSE; + } + + return TRUE; +} + +gboolean +gossip_conf_set_bool (GossipConf *conf, + const gchar *key, + gboolean value) +{ + GossipConfPriv *priv; + + g_return_val_if_fail (GOSSIP_IS_CONF (conf), FALSE); + + gossip_debug (DEBUG_DOMAIN, "Setting bool:'%s' to %d ---> %s", + key, value, value ? "true" : "false"); + + priv = GET_PRIV (conf); + + return gconf_client_set_bool (priv->gconf_client, + key, + value, + NULL); +} + +gboolean +gossip_conf_get_bool (GossipConf *conf, + const gchar *key, + gboolean *value) +{ + GossipConfPriv *priv; + GError *error = NULL; + + *value = FALSE; + + g_return_val_if_fail (GOSSIP_IS_CONF (conf), FALSE); + g_return_val_if_fail (value != NULL, FALSE); + + priv = GET_PRIV (conf); + + *value = gconf_client_get_bool (priv->gconf_client, + key, + &error); + + gossip_debug (DEBUG_DOMAIN, "Getting bool:'%s' (=%d ---> %s), error:'%s'", + key, *value, *value ? "true" : "false", + error ? error->message : "None"); + + if (error) { + g_error_free (error); + return FALSE; + } + + return TRUE; +} + +gboolean +gossip_conf_set_string (GossipConf *conf, + const gchar *key, + const gchar *value) +{ + GossipConfPriv *priv; + + g_return_val_if_fail (GOSSIP_IS_CONF (conf), FALSE); + + gossip_debug (DEBUG_DOMAIN, "Setting string:'%s' to '%s'", + key, value); + + priv = GET_PRIV (conf); + + return gconf_client_set_string (priv->gconf_client, + key, + value, + NULL); +} + +gboolean +gossip_conf_get_string (GossipConf *conf, + const gchar *key, + gchar **value) +{ + GossipConfPriv *priv; + GError *error = NULL; + + *value = NULL; + + g_return_val_if_fail (GOSSIP_IS_CONF (conf), FALSE); + + priv = GET_PRIV (conf); + + *value = gconf_client_get_string (priv->gconf_client, + key, + &error); + + gossip_debug (DEBUG_DOMAIN, "Getting string:'%s' (='%s'), error:'%s'", + key, *value, error ? error->message : "None"); + + if (error) { + g_error_free (error); + return FALSE; + } + + return TRUE; +} + +gboolean +gossip_conf_set_string_list (GossipConf *conf, + const gchar *key, + GSList *value) +{ + GossipConfPriv *priv; + + g_return_val_if_fail (GOSSIP_IS_CONF (conf), FALSE); + + priv = GET_PRIV (conf); + + return gconf_client_set_list (priv->gconf_client, + key, + GCONF_VALUE_STRING, + value, + NULL); +} + +gboolean +gossip_conf_get_string_list (GossipConf *conf, + const gchar *key, + GSList **value) +{ + GossipConfPriv *priv; + GError *error = NULL; + + *value = NULL; + + g_return_val_if_fail (GOSSIP_IS_CONF (conf), FALSE); + + priv = GET_PRIV (conf); + + *value = gconf_client_get_list (priv->gconf_client, + key, + GCONF_VALUE_STRING, + &error); + if (error) { + g_error_free (error); + return FALSE; + } + + return TRUE; +} + +static void +conf_notify_data_free (GossipConfNotifyData *data) +{ + g_object_unref (data->conf); + g_slice_free (GossipConfNotifyData, data); +} + +static void +conf_notify_func (GConfClient *client, + guint id, + GConfEntry *entry, + gpointer user_data) +{ + GossipConfNotifyData *data; + + data = user_data; + + data->func (data->conf, + gconf_entry_get_key (entry), + data->user_data); +} + +guint +gossip_conf_notify_add (GossipConf *conf, + const gchar *key, + GossipConfNotifyFunc func, + gpointer user_data) +{ + GossipConfPriv *priv; + guint id; + GossipConfNotifyData *data; + + g_return_val_if_fail (GOSSIP_IS_CONF (conf), 0); + + priv = GET_PRIV (conf); + + data = g_slice_new (GossipConfNotifyData); + data->func = func; + data->user_data = user_data; + data->conf = g_object_ref (conf); + + id = gconf_client_notify_add (priv->gconf_client, + key, + conf_notify_func, + data, + (GFreeFunc) conf_notify_data_free, + NULL); + + return id; +} + +gboolean +gossip_conf_notify_remove (GossipConf *conf, + guint id) +{ + GossipConfPriv *priv; + + g_return_val_if_fail (GOSSIP_IS_CONF (conf), FALSE); + + priv = GET_PRIV (conf); + + gconf_client_notify_remove (priv->gconf_client, id); + + return TRUE; +} + diff --git a/libempathy/gossip-conf.h b/libempathy/gossip-conf.h new file mode 100644 index 00000000..35fdfb90 --- /dev/null +++ b/libempathy/gossip-conf.h @@ -0,0 +1,87 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2006 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __GOSSIP_CONF_H__ +#define __GOSSIP_CONF_H__ + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GOSSIP_TYPE_CONF (gossip_conf_get_type ()) +#define GOSSIP_CONF(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GOSSIP_TYPE_CONF, GossipConf)) +#define GOSSIP_CONF_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), GOSSIP_TYPE_CONF, GossipConfClass)) +#define GOSSIP_IS_CONF(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GOSSIP_TYPE_CONF)) +#define GOSSIP_IS_CONF_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GOSSIP_TYPE_CONF)) +#define GOSSIP_CONF_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GOSSIP_TYPE_CONF, GossipConfClass)) + +typedef struct _GossipConf GossipConf; +typedef struct _GossipConfClass GossipConfClass; + +struct _GossipConf { + GObject parent; +}; + +struct _GossipConfClass { + GObjectClass parent_class; +}; + +typedef void (*GossipConfNotifyFunc) (GossipConf *conf, + const gchar *key, + gpointer user_data); + +GType gossip_conf_get_type (void) G_GNUC_CONST; +GossipConf *gossip_conf_get (void); +void gossip_conf_shutdown (void); +guint gossip_conf_notify_add (GossipConf *conf, + const gchar *key, + GossipConfNotifyFunc func, + gpointer data); +gboolean gossip_conf_notify_remove (GossipConf *conf, + guint id); +gboolean gossip_conf_set_int (GossipConf *conf, + const gchar *key, + gint value); +gboolean gossip_conf_get_int (GossipConf *conf, + const gchar *key, + gint *value); +gboolean gossip_conf_set_bool (GossipConf *conf, + const gchar *key, + gboolean value); +gboolean gossip_conf_get_bool (GossipConf *conf, + const gchar *key, + gboolean *value); +gboolean gossip_conf_set_string (GossipConf *conf, + const gchar *key, + const gchar *value); +gboolean gossip_conf_get_string (GossipConf *conf, + const gchar *key, + gchar **value); +gboolean gossip_conf_set_string_list (GossipConf *conf, + const gchar *key, + GSList *value); +gboolean gossip_conf_get_string_list (GossipConf *conf, + const gchar *key, + GSList **value); + +G_END_DECLS + +#endif /* __GOSSIP_CONF_H__ */ + diff --git a/libempathy/gossip-contact.c b/libempathy/gossip-contact.c new file mode 100644 index 00000000..6950c89b --- /dev/null +++ b/libempathy/gossip-contact.c @@ -0,0 +1,815 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2004-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Mikael Hallendal <micke@imendio.com> + * Martyn Russell <martyn@imendio.com> + */ + +#include "config.h" + +#include <string.h> + +#include <glib/gi18n.h> + +#include "gossip-contact.h" +#include "gossip-utils.h" +#include "gossip-debug.h" + +#define DEBUG_DOMAIN "Contact" + +#define GET_PRIV(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GOSSIP_TYPE_CONTACT, GossipContactPriv)) + +typedef struct _GossipContactPriv GossipContactPriv; + +struct _GossipContactPriv { + gchar *id; + gchar *name; + guint handle; + GList *presences; + GList *groups; + GossipSubscription subscription; + GossipAvatar *avatar; + McAccount *account; +}; + +static void contact_class_init (GossipContactClass *class); +static void contact_init (GossipContact *contact); +static void contact_finalize (GObject *object); +static void contact_get_property (GObject *object, + guint param_id, + GValue *value, + GParamSpec *pspec); +static void contact_set_property (GObject *object, + guint param_id, + const GValue *value, + GParamSpec *pspec); +static void contact_set_presences (GossipContact *contact, + GList *presences); + +enum { + PROP_0, + PROP_NAME, + PROP_ID, + PROP_PRESENCES, + PROP_GROUPS, + PROP_SUBSCRIPTION, + PROP_AVATAR, + PROP_HANDLE, + PROP_ACCOUNT +}; + +static gpointer parent_class = NULL; + +GType +gossip_contact_get_gtype (void) +{ + static GType type = 0; + + if (!type) { + static const GTypeInfo info = { + sizeof (GossipContactClass), + NULL, /* base_init */ + NULL, /* base_finalize */ + (GClassInitFunc) contact_class_init, + NULL, /* class_finalize */ + NULL, /* class_data */ + sizeof (GossipContact), + 0, /* n_preallocs */ + (GInstanceInitFunc) contact_init + }; + + type = g_type_register_static (G_TYPE_OBJECT, + "GossipContact", + &info, 0); + } + + return type; +} + +static void +contact_class_init (GossipContactClass *class) +{ + GObjectClass *object_class; + + object_class = G_OBJECT_CLASS (class); + parent_class = g_type_class_peek_parent (class); + + object_class->finalize = contact_finalize; + object_class->get_property = contact_get_property; + object_class->set_property = contact_set_property; + + g_object_class_install_property (object_class, + PROP_NAME, + g_param_spec_string ("name", + "Contact Name", + "The name of the contact", + NULL, + G_PARAM_READWRITE)); + + g_object_class_install_property (object_class, + PROP_ID, + g_param_spec_string ("id", + "Contact id", + "String identifying contact", + NULL, + G_PARAM_READWRITE)); + + g_object_class_install_property (object_class, + PROP_PRESENCES, + g_param_spec_pointer ("presences", + "Contact presences", + "Presences of contact", + G_PARAM_READWRITE)); + + g_object_class_install_property (object_class, + PROP_GROUPS, + g_param_spec_pointer ("groups", + "Contact groups", + "Groups of contact", + G_PARAM_READWRITE)); + + g_object_class_install_property (object_class, + PROP_SUBSCRIPTION, + g_param_spec_int ("subscription", + "Contact Subscription", + "The subscription status of the contact", + GOSSIP_SUBSCRIPTION_NONE, + GOSSIP_SUBSCRIPTION_BOTH, + GOSSIP_SUBSCRIPTION_NONE, + G_PARAM_READWRITE)); + + g_object_class_install_property (object_class, + PROP_AVATAR, + g_param_spec_boxed ("avatar", + "Avatar image", + "The avatar image", + GOSSIP_TYPE_AVATAR, + G_PARAM_READWRITE)); + + g_object_class_install_property (object_class, + PROP_ACCOUNT, + g_param_spec_object ("account", + "Contact Account", + "The account associated with the contact", + MC_TYPE_ACCOUNT, + G_PARAM_READWRITE)); + + g_object_class_install_property (object_class, + PROP_HANDLE, + g_param_spec_uint ("handle", + "Contact Handle", + "The handle of the contact", + 0, + G_MAXUINT, + 0, + G_PARAM_READWRITE)); + + g_type_class_add_private (object_class, sizeof (GossipContactPriv)); +} + +static void +contact_init (GossipContact *contact) +{ + GossipContactPriv *priv; + + priv = GET_PRIV (contact); + + priv->name = NULL; + priv->id = NULL; + priv->presences = NULL; + priv->account = NULL; + priv->groups = NULL; + priv->avatar = NULL; + priv->handle = 0; +} + +static void +contact_finalize (GObject *object) +{ + GossipContactPriv *priv; + + priv = GET_PRIV (object); + + gossip_debug (DEBUG_DOMAIN, "finalize: %p", object); + + g_free (priv->name); + g_free (priv->id); + + if (priv->avatar) { + gossip_avatar_unref (priv->avatar); + } + + if (priv->presences) { + g_list_foreach (priv->presences, (GFunc) g_object_unref, NULL); + g_list_free (priv->presences); + } + + if (priv->groups) { + g_list_foreach (priv->groups, (GFunc) g_free, NULL); + g_list_free (priv->groups); + } + + if (priv->account) { + g_object_unref (priv->account); + } + + (G_OBJECT_CLASS (parent_class)->finalize) (object); +} + +static void +contact_get_property (GObject *object, + guint param_id, + GValue *value, + GParamSpec *pspec) +{ + GossipContactPriv *priv; + + priv = GET_PRIV (object); + + switch (param_id) { + case PROP_NAME: + g_value_set_string (value, + gossip_contact_get_name (GOSSIP_CONTACT (object))); + break; + case PROP_ID: + g_value_set_string (value, + gossip_contact_get_id (GOSSIP_CONTACT (object))); + break; + case PROP_PRESENCES: + g_value_set_pointer (value, priv->presences); + break; + case PROP_GROUPS: + g_value_set_pointer (value, priv->groups); + break; + case PROP_SUBSCRIPTION: + g_value_set_int (value, priv->subscription); + break; + case PROP_AVATAR: + g_value_set_boxed (value, priv->avatar); + break; + case PROP_ACCOUNT: + g_value_set_object (value, priv->account); + break; + case PROP_HANDLE: + g_value_set_uint (value, priv->handle); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec); + break; + }; +} + +static void +contact_set_property (GObject *object, + guint param_id, + const GValue *value, + GParamSpec *pspec) +{ + GossipContactPriv *priv; + + priv = GET_PRIV (object); + + switch (param_id) { + case PROP_NAME: + gossip_contact_set_name (GOSSIP_CONTACT (object), + g_value_get_string (value)); + break; + case PROP_ID: + gossip_contact_set_id (GOSSIP_CONTACT (object), + g_value_get_string (value)); + break; + case PROP_PRESENCES: + contact_set_presences (GOSSIP_CONTACT (object), + g_value_get_pointer (value)); + break; + case PROP_GROUPS: + gossip_contact_set_groups (GOSSIP_CONTACT (object), + g_value_get_pointer (value)); + break; + case PROP_SUBSCRIPTION: + gossip_contact_set_subscription (GOSSIP_CONTACT (object), + g_value_get_int (value)); + break; + case PROP_AVATAR: + gossip_contact_set_avatar (GOSSIP_CONTACT (object), + g_value_get_boxed (value)); + break; + case PROP_ACCOUNT: + gossip_contact_set_account (GOSSIP_CONTACT (object), + MC_ACCOUNT (g_value_get_object (value))); + break; + case PROP_HANDLE: + gossip_contact_set_handle (GOSSIP_CONTACT (object), + g_value_get_uint (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec); + break; + }; +} + +GossipContact * +gossip_contact_new (McAccount *account) +{ + return g_object_new (GOSSIP_TYPE_CONTACT, + "account", account, + NULL); +} + +GossipContact * +gossip_contact_new_full (McAccount *account, + const gchar *id, + const gchar *name) +{ + return g_object_new (GOSSIP_TYPE_CONTACT, + "account", account, + "name", name, + "id", id, + NULL); +} + +const gchar * +gossip_contact_get_id (GossipContact *contact) +{ + GossipContactPriv *priv; + + g_return_val_if_fail (GOSSIP_IS_CONTACT (contact), ""); + + priv = GET_PRIV (contact); + + if (priv->id) { + return priv->id; + } + + return ""; +} + +const gchar * +gossip_contact_get_name (GossipContact *contact) +{ + GossipContactPriv *priv; + + g_return_val_if_fail (GOSSIP_IS_CONTACT (contact), ""); + + priv = GET_PRIV (contact); + + if (priv->name == NULL) { + return gossip_contact_get_id (contact); + } + + return priv->name; +} + +GossipAvatar * +gossip_contact_get_avatar (GossipContact *contact) +{ + GossipContactPriv *priv; + + g_return_val_if_fail (GOSSIP_IS_CONTACT (contact), NULL); + + priv = GET_PRIV (contact); + + return priv->avatar; +} + +McAccount * +gossip_contact_get_account (GossipContact *contact) +{ + GossipContactPriv *priv; + + g_return_val_if_fail (GOSSIP_IS_CONTACT (contact), NULL); + + priv = GET_PRIV (contact); + + return priv->account; +} + +GossipPresence * +gossip_contact_get_active_presence (GossipContact *contact) +{ + GossipContactPriv *priv; + + g_return_val_if_fail (GOSSIP_IS_CONTACT (contact), NULL); + + priv = GET_PRIV (contact); + + if (priv->presences) { + /* Highest priority of the presences is first */ + return GOSSIP_PRESENCE (priv->presences->data); + } + + return NULL; +} + +GossipPresence * +gossip_contact_get_presence_for_resource (GossipContact *contact, + const gchar *resource) +{ + GossipContactPriv *priv; + GList *l; + + g_return_val_if_fail (GOSSIP_IS_CONTACT (contact), NULL); + g_return_val_if_fail (resource != NULL, NULL); + + priv = GET_PRIV (contact); + + for (l = priv->presences; l; l = l->next) { + const gchar *p_res; + + p_res = gossip_presence_get_resource (GOSSIP_PRESENCE (l->data)); + if (p_res && strcmp (resource, p_res) == 0) { + return GOSSIP_PRESENCE (l->data); + } + } + + return NULL; +} + +GList * +gossip_contact_get_presence_list (GossipContact *contact) +{ + GossipContactPriv *priv; + + g_return_val_if_fail (GOSSIP_IS_CONTACT (contact), NULL); + + priv = GET_PRIV (contact); + + return priv->presences; +} + +GList * +gossip_contact_get_groups (GossipContact *contact) +{ + GossipContactPriv *priv; + + g_return_val_if_fail (GOSSIP_IS_CONTACT (contact), NULL); + + priv = GET_PRIV (contact); + + return priv->groups; +} + +GossipSubscription +gossip_contact_get_subscription (GossipContact *contact) +{ + GossipContactPriv *priv; + + g_return_val_if_fail (GOSSIP_IS_CONTACT (contact), + GOSSIP_SUBSCRIPTION_NONE); + + priv = GET_PRIV (contact); + + return priv->subscription; +} + +guint +gossip_contact_get_handle (GossipContact *contact) +{ + GossipContactPriv *priv; + + g_return_val_if_fail (GOSSIP_IS_CONTACT (contact), 0); + + priv = GET_PRIV (contact); + + return priv->handle; +} + +void +gossip_contact_set_id (GossipContact *contact, + const gchar *id) +{ + GossipContactPriv *priv; + + g_return_if_fail (GOSSIP_IS_CONTACT (contact)); + g_return_if_fail (id != NULL); + + priv = GET_PRIV (contact); + + g_free (priv->id); + priv->id = g_strdup (id); + + g_object_notify (G_OBJECT (contact), "id"); +} + +void +gossip_contact_set_name (GossipContact *contact, + const gchar *name) +{ + GossipContactPriv *priv; + + g_return_if_fail (GOSSIP_IS_CONTACT (contact)); + g_return_if_fail (name != NULL); + + priv = GET_PRIV (contact); + + g_free (priv->name); + priv->name = g_strdup (name); + + g_object_notify (G_OBJECT (contact), "name"); +} + +static void +contact_set_presences (GossipContact *contact, + GList *presences) +{ + GossipContactPriv *priv; + + priv = GET_PRIV (contact); + + if (priv->presences) { + g_list_foreach (priv->presences, (GFunc) g_object_unref, NULL); + g_list_free (priv->presences); + } + + priv->presences = g_list_copy (presences); + g_list_foreach (priv->presences, (GFunc) g_object_ref, NULL); + + g_object_notify (G_OBJECT (contact), "presences"); +} + +void +gossip_contact_set_avatar (GossipContact *contact, + GossipAvatar *avatar) +{ + GossipContactPriv *priv; + + g_return_if_fail (GOSSIP_IS_CONTACT (contact)); + + priv = GET_PRIV (contact); + + if (priv->avatar) { + gossip_avatar_unref (priv->avatar); + priv->avatar = NULL; + } + + if (avatar) { + priv->avatar = gossip_avatar_ref (avatar); + } + + g_object_notify (G_OBJECT (contact), "avatar"); +} + +void +gossip_contact_set_account (GossipContact *contact, + McAccount *account) +{ + GossipContactPriv *priv; + + g_return_if_fail (GOSSIP_IS_CONTACT (contact)); + g_return_if_fail (MC_IS_ACCOUNT (account)); + + priv = GET_PRIV (contact); + + if (priv->account) { + g_object_unref (priv->account); + } + + if (account) { + priv->account = g_object_ref (account); + } else { + priv->account = NULL; + } + + g_object_notify (G_OBJECT (contact), "account"); +} + +void +gossip_contact_add_presence (GossipContact *contact, + GossipPresence *presence) +{ + GossipContactPriv *priv; + GossipPresence *this_presence; + GList *l; + + g_return_if_fail (GOSSIP_IS_CONTACT (contact)); + g_return_if_fail (GOSSIP_IS_PRESENCE (presence)); + + priv = GET_PRIV (contact); + + for (l = priv->presences; l && presence; l = l->next) { + this_presence = l->data; + + if (gossip_presence_resource_equal (this_presence, presence)) { + gint ref_count; + + ref_count = G_OBJECT (presence)->ref_count; + + /* Remove old presence for this resource, we + * would use g_list_remove_all() here but we + * want to make sure we unref for each + * instance we find it in the list. + */ + priv->presences = g_list_remove (priv->presences, this_presence); + g_object_unref (this_presence); + + if (!priv->presences || ref_count <= 1) { + break; + } + + /* Reset list to beginnging to make sure we + * didn't miss any duplicates. + */ + l = priv->presences; + } + } + + /* Add new presence */ + priv->presences = g_list_insert_sorted (priv->presences, + g_object_ref (presence), + gossip_presence_sort_func); + + g_object_notify (G_OBJECT (contact), "presences"); +} + +void +gossip_contact_remove_presence (GossipContact *contact, + GossipPresence *presence) +{ + GossipContactPriv *priv; + GossipPresence *this_presence; + GList *l; + + g_return_if_fail (GOSSIP_IS_CONTACT (contact)); + g_return_if_fail (GOSSIP_IS_PRESENCE (presence)); + + priv = GET_PRIV (contact); + + for (l = priv->presences; l; l = l->next) { + this_presence = l->data; + + if (gossip_presence_resource_equal (this_presence, presence)) { + gint ref_count; + + ref_count = G_OBJECT (presence)->ref_count; + + /* Remove old presence for this resource, we + * would use g_list_remove_all() here but we + * want to make sure we unref for each + * instance we find it in the list. + */ + priv->presences = g_list_remove (priv->presences, this_presence); + g_object_unref (this_presence); + + if (!priv->presences || ref_count <= 1) { + break; + } + + /* Reset list to beginnging to make sure we + * didn't miss any duplicates. + */ + l = priv->presences; + } + } + + priv->presences = g_list_sort (priv->presences, + gossip_presence_sort_func); + + g_object_notify (G_OBJECT (contact), "presences"); +} + +void +gossip_contact_set_groups (GossipContact *contact, + GList *groups) +{ + GossipContactPriv *priv; + GList *old_groups, *l; + + g_return_if_fail (GOSSIP_IS_CONTACT (contact)); + + priv = GET_PRIV (contact); + + old_groups = priv->groups; + priv->groups = NULL; + + for (l = groups; l; l = l->next) { + priv->groups = g_list_append (priv->groups, + g_strdup (l->data)); + } + + g_list_foreach (old_groups, (GFunc) g_free, NULL); + g_list_free (old_groups); + + g_object_notify (G_OBJECT (contact), "groups"); +} + +void +gossip_contact_set_subscription (GossipContact *contact, + GossipSubscription subscription) +{ + GossipContactPriv *priv; + + g_return_if_fail (GOSSIP_IS_CONTACT (contact)); + + priv = GET_PRIV (contact); + + priv->subscription = subscription; + + g_object_notify (G_OBJECT (contact), "subscription"); +} + +void +gossip_contact_set_handle (GossipContact *contact, + guint handle) +{ + GossipContactPriv *priv; + + priv = GET_PRIV (contact); + + priv->handle = handle; + + g_object_notify (G_OBJECT (contact), "handle"); +} + +gboolean +gossip_contact_is_online (GossipContact *contact) +{ + GossipContactPriv *priv; + + g_return_val_if_fail (GOSSIP_IS_CONTACT (contact), FALSE); + + priv = GET_PRIV (contact); + + if (priv->presences == NULL) { + return FALSE; + } + + return TRUE; +} + +const gchar * +gossip_contact_get_status (GossipContact *contact) +{ + GossipContactPriv *priv; + + g_return_val_if_fail (GOSSIP_IS_CONTACT (contact), ""); + + priv = GET_PRIV (contact); + + if (priv->presences) { + GossipPresence *p; + const gchar *status; + + p = GOSSIP_PRESENCE (priv->presences->data); + status = gossip_presence_get_status (p); + if (!status) { + status = gossip_presence_state_get_default_status (gossip_presence_get_state (p)); + } + return status; + } else { + return _("Offline"); + } +} + +gboolean +gossip_contact_equal (gconstpointer v1, + gconstpointer v2) +{ + McAccount *account_a; + McAccount *account_b; + const gchar *id_a; + const gchar *id_b; + + g_return_val_if_fail (GOSSIP_IS_CONTACT (v1), FALSE); + g_return_val_if_fail (GOSSIP_IS_CONTACT (v2), FALSE); + + account_a = gossip_contact_get_account (GOSSIP_CONTACT (v1)); + account_b = gossip_contact_get_account (GOSSIP_CONTACT (v2)); + + id_a = gossip_contact_get_id (GOSSIP_CONTACT (v1)); + id_b = gossip_contact_get_id (GOSSIP_CONTACT (v2)); + + return gossip_account_equal (account_a, account_b) && g_str_equal (id_a, id_b); +} + +guint +gossip_contact_hash (gconstpointer key) +{ + GossipContactPriv *priv; + guint hash; + + g_return_val_if_fail (GOSSIP_IS_CONTACT (key), +1); + + priv = GET_PRIV (GOSSIP_CONTACT (key)); + + hash = gossip_account_hash (gossip_contact_get_account (GOSSIP_CONTACT (key))); + hash += g_str_hash (gossip_contact_get_id (GOSSIP_CONTACT (key))); + + return hash; +} + diff --git a/libempathy/gossip-contact.h b/libempathy/gossip-contact.h new file mode 100644 index 00000000..92caee9e --- /dev/null +++ b/libempathy/gossip-contact.h @@ -0,0 +1,102 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2004 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __GOSSIP_CONTACT_H__ +#define __GOSSIP_CONTACT_H__ + +#include <glib-object.h> + +#include <libmissioncontrol/mc-account.h> + +#include "gossip-avatar.h" +#include "gossip-presence.h" + +G_BEGIN_DECLS + +#define GOSSIP_TYPE_CONTACT (gossip_contact_get_gtype ()) +#define GOSSIP_CONTACT(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GOSSIP_TYPE_CONTACT, GossipContact)) +#define GOSSIP_CONTACT_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), GOSSIP_TYPE_CONTACT, GossipContactClass)) +#define GOSSIP_IS_CONTACT(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GOSSIP_TYPE_CONTACT)) +#define GOSSIP_IS_CONTACT_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GOSSIP_TYPE_CONTACT)) +#define GOSSIP_CONTACT_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GOSSIP_TYPE_CONTACT, GossipContactClass)) + +typedef struct _GossipContact GossipContact; +typedef struct _GossipContactClass GossipContactClass; + +struct _GossipContact { + GObject parent; +}; + +struct _GossipContactClass { + GObjectClass parent_class; +}; + +typedef enum { + GOSSIP_SUBSCRIPTION_NONE = 0, + GOSSIP_SUBSCRIPTION_TO = 1 << 0, /* We send our presence to that contact */ + GOSSIP_SUBSCRIPTION_FROM = 1 << 1, /* That contact sends his presence to us */ + GOSSIP_SUBSCRIPTION_BOTH = GOSSIP_SUBSCRIPTION_TO | GOSSIP_SUBSCRIPTION_FROM +} GossipSubscription; + +GType gossip_contact_get_gtype (void) G_GNUC_CONST; + +GossipContact * gossip_contact_new (McAccount *account); +GossipContact * gossip_contact_new_full (McAccount *account, + const gchar *id, + const gchar *name); +const gchar * gossip_contact_get_id (GossipContact *contact); +const gchar * gossip_contact_get_name (GossipContact *contact); +GossipAvatar * gossip_contact_get_avatar (GossipContact *contact); +McAccount * gossip_contact_get_account (GossipContact *contact); +void gossip_contact_add_presence (GossipContact *contact, + GossipPresence *presence); +void gossip_contact_remove_presence (GossipContact *contact, + GossipPresence *presence); +GossipPresence * gossip_contact_get_presence_for_resource (GossipContact *contact, + const gchar *resource); +GossipPresence * gossip_contact_get_active_presence (GossipContact *contact); +GList * gossip_contact_get_presence_list (GossipContact *contact); +GList * gossip_contact_get_groups (GossipContact *contact); +GossipSubscription gossip_contact_get_subscription (GossipContact *contact); +guint gossip_contact_get_handle (GossipContact *contact); +void gossip_contact_set_id (GossipContact *contact, + const gchar *id); +void gossip_contact_set_name (GossipContact *contact, + const gchar *name); +void gossip_contact_set_avatar (GossipContact *contact, + GossipAvatar *avatar); +void gossip_contact_set_account (GossipContact *contact, + McAccount *account); +void gossip_contact_set_groups (GossipContact *contact, + GList *categories); +void gossip_contact_set_subscription (GossipContact *contact, + GossipSubscription subscription); +void gossip_contact_set_handle (GossipContact *contact, + guint handle); +gboolean gossip_contact_is_online (GossipContact *contact); +const gchar * gossip_contact_get_status (GossipContact *contact); +gboolean gossip_contact_equal (gconstpointer v1, + gconstpointer v2); +guint gossip_contact_hash (gconstpointer key); + +G_END_DECLS + +#endif /* __GOSSIP_CONTACT_H__ */ + diff --git a/libempathy/gossip-debug.c b/libempathy/gossip-debug.c new file mode 100644 index 00000000..a6bbeea2 --- /dev/null +++ b/libempathy/gossip-debug.c @@ -0,0 +1,92 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2006-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Richard Hult <richard@imendio.com> + */ + +#include "config.h" + +#include <stdarg.h> +#include <string.h> + +#include <glib.h> +#include <glib/gprintf.h> + +/* Set EMPATHY_DEBUG to a colon/comma/space separated list of domains, or "all" + * to get all debug output. + */ + +#include "gossip-debug.h" + +static gchar **debug_strv; +static gboolean all_domains = FALSE; + +static void +debug_init (void) +{ + static gboolean inited = FALSE; + + if (!inited) { + const gchar *env; + gint i; + + env = g_getenv ("EMPATHY_DEBUG"); + + if (env) { + debug_strv = g_strsplit_set (env, ":, ", 0); + } else { + debug_strv = NULL; + } + + for (i = 0; debug_strv && debug_strv[i]; i++) { + if (strcmp ("all", debug_strv[i]) == 0) { + all_domains = TRUE; + } + } + + inited = TRUE; + } +} + +void +gossip_debug_impl (const gchar *domain, const gchar *msg, ...) +{ + gint i; + + g_return_if_fail (domain != NULL); + g_return_if_fail (msg != NULL); + + debug_init (); + + for (i = 0; debug_strv && debug_strv[i]; i++) { + if (all_domains || strcmp (domain, debug_strv[i]) == 0) { + va_list args; + + g_print ("%s: ", domain); + + va_start (args, msg); + g_vprintf (msg, args); + va_end (args); + + g_print ("\n"); + break; + } + } +} + diff --git a/libempathy/gossip-debug.h b/libempathy/gossip-debug.h new file mode 100644 index 00000000..39dae0f1 --- /dev/null +++ b/libempathy/gossip-debug.h @@ -0,0 +1,53 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2006 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __GOSSIP_DEBUG_H__ +#define __GOSSIP_DEBUG_H__ + +#include <glib.h> + +G_BEGIN_DECLS + +#ifdef G_HAVE_ISO_VARARGS +# ifdef GOSSIP_DISABLE_DEBUG +# define gossip_debug(...) +# else +# define gossip_debug(...) gossip_debug_impl (__VA_ARGS__) +# endif +#elif defined(G_HAVE_GNUC_VARARGS) +# if GOSSIP_DISABLE_DEBUG +# define gossip_debug(fmt...) +# else +# define gossip_debug(fmt...) gossip_debug_impl(fmt) +# endif +#else +# if GOSSIP_DISABLE_DEBUG +# define gossip_debug(x) +# else +# define gossip_debug gossip_debug_impl +# endif +#endif + +void gossip_debug_impl (const gchar *domain, const gchar *msg, ...); + +G_END_DECLS + +#endif /* __GOSSIP_DEBUG_H__ */ + diff --git a/libempathy/gossip-message.c b/libempathy/gossip-message.c new file mode 100644 index 00000000..85889e7d --- /dev/null +++ b/libempathy/gossip-message.c @@ -0,0 +1,365 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2004-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Mikael Hallendal <micke@imendio.com> + */ + +#include "config.h" + +#include "gossip-message.h" + +#define GET_PRIV(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GOSSIP_TYPE_MESSAGE, GossipMessagePriv)) + +typedef struct _GossipMessagePriv GossipMessagePriv; + +struct _GossipMessagePriv { + GossipMessageType type; + GossipContact *sender; + gchar *body; + GossipTime timestamp; + +}; + +static void gossip_message_class_init (GossipMessageClass *class); +static void gossip_message_init (GossipMessage *message); +static void gossip_message_finalize (GObject *object); +static void message_get_property (GObject *object, + guint param_id, + GValue *value, + GParamSpec *pspec); +static void message_set_property (GObject *object, + guint param_id, + const GValue *value, + GParamSpec *pspec); + +enum { + PROP_0, + PROP_TYPE, + PROP_SENDER, + PROP_BODY, + PROP_TIMESTAMP, +}; + +static gpointer parent_class = NULL; + +GType +gossip_message_get_gtype (void) +{ + static GType type = 0; + + if (!type) { + static const GTypeInfo info = { + sizeof (GossipMessageClass), + NULL, /* base_init */ + NULL, /* base_finalize */ + (GClassInitFunc) gossip_message_class_init, + NULL, /* class_finalize */ + NULL, /* class_data */ + sizeof (GossipMessage), + 0, /* n_preallocs */ + (GInstanceInitFunc) gossip_message_init + }; + + type = g_type_register_static (G_TYPE_OBJECT, + "GossipMessage", + &info, 0); + } + + return type; +} + +static void +gossip_message_class_init (GossipMessageClass *class) +{ + GObjectClass *object_class; + + object_class = G_OBJECT_CLASS (class); + parent_class = g_type_class_peek_parent (class); + + object_class->finalize = gossip_message_finalize; + object_class->get_property = message_get_property; + object_class->set_property = message_set_property; + + g_object_class_install_property (object_class, + PROP_TYPE, + g_param_spec_int ("type", + "Message Type", + "The type of message", + GOSSIP_MESSAGE_TYPE_NORMAL, + GOSSIP_MESSAGE_TYPE_LAST, + GOSSIP_MESSAGE_TYPE_NORMAL, + G_PARAM_READWRITE)); + g_object_class_install_property (object_class, + PROP_SENDER, + g_param_spec_object ("sender", + "Message Sender", + "The sender of the message", + GOSSIP_TYPE_CONTACT, + G_PARAM_READWRITE)); + + g_object_class_install_property (object_class, + PROP_BODY, + g_param_spec_string ("body", + "Message Body", + "The content of the message", + NULL, + G_PARAM_READWRITE)); + g_object_class_install_property (object_class, + PROP_TIMESTAMP, + g_param_spec_long ("timestamp", + "timestamp", + "timestamp", + -1, + G_MAXLONG, + -1, + G_PARAM_READWRITE)); + + + g_type_class_add_private (object_class, sizeof (GossipMessagePriv)); + +} + +static void +gossip_message_init (GossipMessage *message) +{ + GossipMessagePriv *priv; + + priv = GET_PRIV (message); + + priv->type = GOSSIP_MESSAGE_TYPE_NORMAL; + priv->sender = NULL; + priv->body = NULL; + priv->timestamp = gossip_time_get_current (); +} + +static void +gossip_message_finalize (GObject *object) +{ + GossipMessagePriv *priv; + + priv = GET_PRIV (object); + + if (priv->sender) { + g_object_unref (priv->sender); + } + + g_free (priv->body); + + (G_OBJECT_CLASS (parent_class)->finalize) (object); +} + +static void +message_get_property (GObject *object, + guint param_id, + GValue *value, + GParamSpec *pspec) +{ + GossipMessagePriv *priv; + + priv = GET_PRIV (object); + + switch (param_id) { + case PROP_TYPE: + g_value_set_int (value, priv->type); + break; + case PROP_SENDER: + g_value_set_object (value, priv->sender); + break; + case PROP_BODY: + g_value_set_string (value, priv->body); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec); + break; + }; +} + +static void +message_set_property (GObject *object, + guint param_id, + const GValue *value, + GParamSpec *pspec) +{ + GossipMessagePriv *priv; + + priv = GET_PRIV (object); + + switch (param_id) { + case PROP_TYPE: + gossip_message_set_type (GOSSIP_MESSAGE (object), + g_value_get_int (value)); + break; + case PROP_SENDER: + gossip_message_set_sender (GOSSIP_MESSAGE (object), + GOSSIP_CONTACT (g_value_get_object (value))); + break; + case PROP_BODY: + gossip_message_set_body (GOSSIP_MESSAGE (object), + g_value_get_string (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec); + break; + }; +} + +GossipMessage * +gossip_message_new (const gchar *body) +{ + return g_object_new (GOSSIP_TYPE_MESSAGE, + "body", body, + NULL); +} + +GossipMessageType +gossip_message_get_type (GossipMessage *message) +{ + GossipMessagePriv *priv; + + g_return_val_if_fail (GOSSIP_IS_MESSAGE (message), + GOSSIP_MESSAGE_TYPE_NORMAL); + + priv = GET_PRIV (message); + + return priv->type; +} + +void +gossip_message_set_type (GossipMessage *message, + GossipMessageType type) +{ + GossipMessagePriv *priv; + + g_return_if_fail (GOSSIP_IS_MESSAGE (message)); + + priv = GET_PRIV (message); + + priv->type = type; + + g_object_notify (G_OBJECT (message), "type"); +} + +GossipContact * +gossip_message_get_sender (GossipMessage *message) +{ + GossipMessagePriv *priv; + + g_return_val_if_fail (GOSSIP_IS_MESSAGE (message), NULL); + + priv = GET_PRIV (message); + + return priv->sender; +} + +void +gossip_message_set_sender (GossipMessage *message, GossipContact *contact) +{ + GossipMessagePriv *priv; + GossipContact *old_sender; + + g_return_if_fail (GOSSIP_IS_MESSAGE (message)); + g_return_if_fail (GOSSIP_IS_CONTACT (contact)); + + priv = GET_PRIV (message); + + old_sender = priv->sender; + priv->sender = g_object_ref (contact); + + if (old_sender) { + g_object_unref (old_sender); + } + + g_object_notify (G_OBJECT (message), "sender"); +} + +const gchar * +gossip_message_get_body (GossipMessage *message) +{ + GossipMessagePriv *priv; + + g_return_val_if_fail (GOSSIP_IS_MESSAGE (message), NULL); + + priv = GET_PRIV (message); + + return priv->body; +} + +void +gossip_message_set_body (GossipMessage *message, + const gchar *body) +{ + GossipMessagePriv *priv; + GossipMessageType type; + + g_return_if_fail (GOSSIP_IS_MESSAGE (message)); + + priv = GET_PRIV (message); + + g_free (priv->body); + priv->body = NULL; + + type = GOSSIP_MESSAGE_TYPE_NORMAL; + if (g_str_has_prefix (body, "/me")) { + type = GOSSIP_MESSAGE_TYPE_ACTION; + body += 4; + } + + if (body) { + priv->body = g_strdup (body); + } + + if (type != priv->type) { + gossip_message_set_type (message, type); + } + + g_object_notify (G_OBJECT (message), "body"); +} + +GossipTime +gossip_message_get_timestamp (GossipMessage *message) +{ + GossipMessagePriv *priv; + + g_return_val_if_fail (GOSSIP_IS_MESSAGE (message), -1); + + priv = GET_PRIV (message); + + return priv->timestamp; +} + +void +gossip_message_set_timestamp (GossipMessage *message, + GossipTime timestamp) +{ + GossipMessagePriv *priv; + + g_return_if_fail (GOSSIP_IS_MESSAGE (message)); + g_return_if_fail (timestamp >= -1); + + priv = GET_PRIV (message); + + if (timestamp <= 0) { + priv->timestamp = gossip_time_get_current (); + } else { + priv->timestamp = timestamp; + } + + g_object_notify (G_OBJECT (message), "timestamp"); +} + diff --git a/libempathy/gossip-message.h b/libempathy/gossip-message.h new file mode 100644 index 00000000..8defba7e --- /dev/null +++ b/libempathy/gossip-message.h @@ -0,0 +1,75 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2004 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __GOSSIP_MESSAGE_H__ +#define __GOSSIP_MESSAGE_H__ + +#include <glib-object.h> + +#include "gossip-contact.h" +#include "gossip-time.h" + +G_BEGIN_DECLS + +#define GOSSIP_TYPE_MESSAGE (gossip_message_get_gtype ()) +#define GOSSIP_MESSAGE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GOSSIP_TYPE_MESSAGE, GossipMessage)) +#define GOSSIP_MESSAGE_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), GOSSIP_TYPE_MESSAGE, GossipMessageClass)) +#define GOSSIP_IS_MESSAGE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GOSSIP_TYPE_MESSAGE)) +#define GOSSIP_IS_MESSAGE_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GOSSIP_TYPE_MESSAGE)) +#define GOSSIP_MESSAGE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GOSSIP_TYPE_MESSAGE, GossipMessageClass)) + +typedef struct _GossipMessage GossipMessage; +typedef struct _GossipMessageClass GossipMessageClass; + +struct _GossipMessage { + GObject parent; +}; + +struct _GossipMessageClass { + GObjectClass parent_class; +}; + +typedef enum { + GOSSIP_MESSAGE_TYPE_NORMAL, + GOSSIP_MESSAGE_TYPE_ACTION, + GOSSIP_MESSAGE_TYPE_NOTICE, + GOSSIP_MESSAGE_TYPE_AUTO_REPLY, + GOSSIP_MESSAGE_TYPE_LAST +} GossipMessageType; + +GType gossip_message_get_gtype (void) G_GNUC_CONST; +GossipMessage * gossip_message_new (const gchar *body); +GossipMessageType gossip_message_get_type (GossipMessage *message); +void gossip_message_set_type (GossipMessage *message, + GossipMessageType type); +GossipContact * gossip_message_get_sender (GossipMessage *message); +void gossip_message_set_sender (GossipMessage *message, + GossipContact *contact); +const gchar * gossip_message_get_body (GossipMessage *message); +void gossip_message_set_body (GossipMessage *message, + const gchar *body); +/* What return value should we have here? */ +GossipTime gossip_message_get_timestamp (GossipMessage *message); +void gossip_message_set_timestamp (GossipMessage *message, + GossipTime timestamp); + +G_END_DECLS + +#endif /* __GOSSIP_MESSAGE_H__ */ diff --git a/libempathy/gossip-paths.c b/libempathy/gossip-paths.c new file mode 100644 index 00000000..aa97bdcc --- /dev/null +++ b/libempathy/gossip-paths.c @@ -0,0 +1,51 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2006-2007 Imendio AB + * + * This library 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 library 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 library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Richard Hult <richard@imendio.com> + */ + +#include "config.h" + +#include "gossip-paths.h" + +#define EMPATHY "empathy" + +gchar * +gossip_paths_get_glade_path (const gchar *filename) +{ + return g_build_filename (DATADIR, EMPATHY, filename, NULL); +} + +gchar * +gossip_paths_get_image_path (const gchar *filename) +{ + return g_build_filename (DATADIR, EMPATHY, filename, NULL); +} + +gchar * +gossip_paths_get_dtd_path (const gchar *filename) +{ + return g_build_filename (DATADIR, EMPATHY, filename, NULL); +} + +gchar * +gossip_paths_get_locale_path () +{ + return g_strdup (LOCALEDIR); +} diff --git a/libempathy/gossip-paths.h b/libempathy/gossip-paths.h new file mode 100644 index 00000000..e00a33e2 --- /dev/null +++ b/libempathy/gossip-paths.h @@ -0,0 +1,36 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2006 Imendio AB + * + * This library 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 library 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 library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __GOSSIP_PATHS_H__ +#define __GOSSIP_PATHS_H__ + +#include <glib.h> + +G_BEGIN_DECLS + +gchar *gossip_paths_get_glade_path (const gchar *filename); +gchar *gossip_paths_get_image_path (const gchar *filename); +gchar *gossip_paths_get_dtd_path (const gchar *filename); +gchar *gossip_paths_get_sound_path (const gchar *filename); +gchar *gossip_paths_get_locale_path (void); + +G_END_DECLS + +#endif /* __GOSSIP_PATHS_H__ */ diff --git a/libempathy/gossip-presence.c b/libempathy/gossip-presence.c new file mode 100644 index 00000000..e41ae554 --- /dev/null +++ b/libempathy/gossip-presence.c @@ -0,0 +1,441 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2004-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Mikael Hallendal <micke@imendio.com> + */ + +#include "config.h" + +#include <string.h> + +#include <glib/gi18n.h> + +#include "gossip-presence.h" +#include "gossip-time.h" + +#define GET_PRIV(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GOSSIP_TYPE_PRESENCE, GossipPresencePriv)) + +typedef struct _GossipPresencePriv GossipPresencePriv; + +struct _GossipPresencePriv { + GossipPresenceState state; + + gchar *status; + gchar *resource; + + gint priority; + GossipTime timestamp; +}; + +static void presence_finalize (GObject *object); +static void presence_get_property (GObject *object, + guint param_id, + GValue *value, + GParamSpec *pspec); +static void presence_set_property (GObject *object, + guint param_id, + const GValue *value, + GParamSpec *pspec); + +enum { + PROP_0, + PROP_STATE, + PROP_STATUS, + PROP_RESOURCE, + PROP_PRIORITY +}; + +G_DEFINE_TYPE (GossipPresence, gossip_presence, G_TYPE_OBJECT); + +static void +gossip_presence_class_init (GossipPresenceClass *class) +{ + GObjectClass *object_class; + + object_class = G_OBJECT_CLASS (class); + + object_class->finalize = presence_finalize; + object_class->get_property = presence_get_property; + object_class->set_property = presence_set_property; + + g_object_class_install_property (object_class, + PROP_STATE, + g_param_spec_int ("state", + "Presence State", + "The current state of the presence", + GOSSIP_PRESENCE_STATE_AVAILABLE, + GOSSIP_PRESENCE_STATE_EXT_AWAY, + GOSSIP_PRESENCE_STATE_AVAILABLE, + G_PARAM_READWRITE)); + g_object_class_install_property (object_class, + PROP_STATUS, + g_param_spec_string ("status", + "Presence Status", + "Status string set on presence", + NULL, + G_PARAM_READWRITE)); + g_object_class_install_property (object_class, + PROP_RESOURCE, + g_param_spec_string ("resource", + "Presence Resource", + "Resource that this presence is for", + NULL, + G_PARAM_READWRITE)); + g_object_class_install_property (object_class, + PROP_PRIORITY, + g_param_spec_int ("priority", + "Presence Priority", + "Priority value of presence", + G_MININT, + G_MAXINT, + 0, + G_PARAM_READWRITE)); + + g_type_class_add_private (object_class, sizeof (GossipPresencePriv)); +} + +static void +gossip_presence_init (GossipPresence *presence) +{ + GossipPresencePriv *priv; + + priv = GET_PRIV (presence); + + priv->state = GOSSIP_PRESENCE_STATE_AVAILABLE; + + priv->status = NULL; + priv->resource = NULL; + + priv->priority = 0; + + priv->timestamp = gossip_time_get_current (); +} + +static void +presence_finalize (GObject *object) +{ + GossipPresencePriv *priv; + + priv = GET_PRIV (object); + + g_free (priv->status); + g_free (priv->resource); + + (G_OBJECT_CLASS (gossip_presence_parent_class)->finalize) (object); +} + +static void +presence_get_property (GObject *object, + guint param_id, + GValue *value, + GParamSpec *pspec) +{ + GossipPresencePriv *priv; + + priv = GET_PRIV (object); + + switch (param_id) { + case PROP_STATE: + g_value_set_int (value, priv->state); + break; + case PROP_STATUS: + g_value_set_string (value, + gossip_presence_get_status (GOSSIP_PRESENCE (object))); + break; + case PROP_RESOURCE: + g_value_set_string (value, + gossip_presence_get_resource (GOSSIP_PRESENCE (object))); + break; + case PROP_PRIORITY: + g_value_set_int (value, priv->priority); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec); + break; + } +} +static void +presence_set_property (GObject *object, + guint param_id, + const GValue *value, + GParamSpec *pspec) +{ + GossipPresencePriv *priv; + + priv = GET_PRIV (object); + + switch (param_id) { + case PROP_STATE: + priv->state = g_value_get_int (value); + break; + case PROP_STATUS: + gossip_presence_set_status (GOSSIP_PRESENCE (object), + g_value_get_string (value)); + break; + case PROP_RESOURCE: + gossip_presence_set_resource (GOSSIP_PRESENCE (object), + g_value_get_string (value)); + break; + case PROP_PRIORITY: + priv->priority = g_value_get_int (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec); + break; + } +} + +GossipPresence * +gossip_presence_new (void) +{ + return g_object_new (GOSSIP_TYPE_PRESENCE, NULL); +} + +GossipPresence * +gossip_presence_new_full (GossipPresenceState state, + const gchar *status) +{ + return g_object_new (GOSSIP_TYPE_PRESENCE, + "state", state, + "status", status, + NULL); +} + +const gchar * +gossip_presence_get_resource (GossipPresence *presence) +{ + GossipPresencePriv *priv; + + g_return_val_if_fail (GOSSIP_IS_PRESENCE (presence), NULL); + + priv = GET_PRIV (presence); + + if (priv->resource) { + return priv->resource; + } + + return NULL; +} + +const gchar * +gossip_presence_get_status (GossipPresence *presence) +{ + GossipPresencePriv *priv; + + g_return_val_if_fail (GOSSIP_IS_PRESENCE (presence), + _("Offline")); + + priv = GET_PRIV (presence); + + return priv->status; +} + +gint +gossip_presence_get_priority (GossipPresence *presence) +{ + GossipPresencePriv *priv; + + priv = GET_PRIV (presence); + g_return_val_if_fail (GOSSIP_IS_PRESENCE (presence), 0); + + return priv->priority; +} + +void +gossip_presence_set_resource (GossipPresence *presence, + const gchar *resource) +{ + GossipPresencePriv *priv; + + g_return_if_fail (GOSSIP_IS_PRESENCE (presence)); + g_return_if_fail (resource != NULL); + + priv = GET_PRIV (presence); + + g_free (priv->resource); + priv->resource = g_strdup (resource); + + g_object_notify (G_OBJECT (presence), "resource"); +} + +GossipPresenceState +gossip_presence_get_state (GossipPresence *presence) +{ + GossipPresencePriv *priv; + + g_return_val_if_fail (GOSSIP_IS_PRESENCE (presence), + GOSSIP_PRESENCE_STATE_AVAILABLE); + + priv = GET_PRIV (presence); + + return priv->state; +} + +void +gossip_presence_set_state (GossipPresence *presence, + GossipPresenceState state) +{ + GossipPresencePriv *priv; + + g_return_if_fail (GOSSIP_IS_PRESENCE (presence)); + + priv = GET_PRIV (presence); + + priv->state = state; + + g_object_notify (G_OBJECT (presence), "state"); +} + +void +gossip_presence_set_status (GossipPresence *presence, + const gchar *status) +{ + GossipPresencePriv *priv; + + priv = GET_PRIV (presence); + g_return_if_fail (GOSSIP_IS_PRESENCE (presence)); + + g_free (priv->status); + + if (status) { + priv->status = g_strdup (status); + } else { + priv->status = NULL; + } + + g_object_notify (G_OBJECT (presence), "status"); +} + +void +gossip_presence_set_priority (GossipPresence *presence, + gint priority) +{ + GossipPresencePriv *priv; + + g_return_if_fail (GOSSIP_IS_PRESENCE (presence)); + + priv = GET_PRIV (presence); + + priv->priority = priority; + + g_object_notify (G_OBJECT (presence), "priority"); +} + +gboolean +gossip_presence_resource_equal (gconstpointer a, + gconstpointer b) +{ + GossipPresencePriv *priv1; + GossipPresencePriv *priv2; + + g_return_val_if_fail (GOSSIP_IS_PRESENCE (a), FALSE); + g_return_val_if_fail (GOSSIP_IS_PRESENCE (b), FALSE); + + priv1 = GET_PRIV (a); + priv2 = GET_PRIV (b); + + if (!priv1->resource) { + if (!priv2->resource) { + return TRUE; + } + + return FALSE; + } + + if (!priv2->resource) { + return FALSE; + } + + if (strcmp (priv1->resource, priv2->resource) == 0) { + return TRUE; + } + + return FALSE; +} + +gint +gossip_presence_sort_func (gconstpointer a, + gconstpointer b) +{ + GossipPresencePriv *priv_a; + GossipPresencePriv *priv_b; + gint diff; + + g_return_val_if_fail (GOSSIP_IS_PRESENCE (a), 0); + g_return_val_if_fail (GOSSIP_IS_PRESENCE (b), 0); + + /* We sort here by priority AND status, in theory, the + * priority would be enough for JUST Jabber contacts which + * actually abide to the protocol, but for other protocols and + * dodgy clients, we will sort by: + * + * 1. State + * 2. Priority + * 3. Time it was set (most recent first). + */ + + priv_a = GET_PRIV (a); + priv_b = GET_PRIV (b); + + /* 1. State */ + diff = priv_a->state - priv_b->state; + if (diff != 0) { + return diff < 1 ? -1 : +1; + } + + /* 2. Priority */ + diff = priv_a->priority - priv_b->priority; + if (diff != 0) { + return diff < 1 ? -1 : +1; + } + + /* 3. Time (newest first) */ + diff = priv_b->timestamp - priv_a->timestamp; + if (diff != 0) { + return diff < 1 ? -1 : +1; + } + + /* No real difference, except maybe resource */ + return 0; +} + +const gchar * +gossip_presence_state_get_default_status (GossipPresenceState state) +{ + switch (state) { + case GOSSIP_PRESENCE_STATE_AVAILABLE: + return _("Available"); + break; + + case GOSSIP_PRESENCE_STATE_BUSY: + return _("Busy"); + break; + + case GOSSIP_PRESENCE_STATE_AWAY: + case GOSSIP_PRESENCE_STATE_EXT_AWAY: + return _("Away"); + break; + + case GOSSIP_PRESENCE_STATE_HIDDEN: + case GOSSIP_PRESENCE_STATE_UNAVAILABLE: + return _("Unavailable"); + } + + return _("Available"); +} diff --git a/libempathy/gossip-presence.h b/libempathy/gossip-presence.h new file mode 100644 index 00000000..64a0b8ec --- /dev/null +++ b/libempathy/gossip-presence.h @@ -0,0 +1,84 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2004 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __GOSSIP_PRESENCE_H__ +#define __GOSSIP_PRESENCE_H__ + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GOSSIP_TYPE_PRESENCE (gossip_presence_get_type ()) +#define GOSSIP_PRESENCE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GOSSIP_TYPE_PRESENCE, GossipPresence)) +#define GOSSIP_PRESENCE_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), GOSSIP_TYPE_PRESENCE, GossipPresenceClass)) +#define GOSSIP_IS_PRESENCE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GOSSIP_TYPE_PRESENCE)) +#define GOSSIP_IS_PRESENCE_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GOSSIP_TYPE_PRESENCE)) +#define GOSSIP_PRESENCE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GOSSIP_TYPE_PRESENCE, GossipPresenceClass)) + +typedef struct _GossipPresence GossipPresence; +typedef struct _GossipPresenceClass GossipPresenceClass; + +struct _GossipPresence { + GObject parent; +}; + +struct _GossipPresenceClass { + GObjectClass parent_class; +}; + +typedef enum { + GOSSIP_PRESENCE_STATE_AVAILABLE, + GOSSIP_PRESENCE_STATE_BUSY, + GOSSIP_PRESENCE_STATE_AWAY, + GOSSIP_PRESENCE_STATE_EXT_AWAY, + GOSSIP_PRESENCE_STATE_HIDDEN, /* When you appear offline to others */ + GOSSIP_PRESENCE_STATE_UNAVAILABLE, +} GossipPresenceState; + +GType gossip_presence_get_type (void) G_GNUC_CONST; + +GossipPresence * gossip_presence_new (void); +GossipPresence * gossip_presence_new_full (GossipPresenceState state, + const gchar *status); + +const gchar * gossip_presence_get_resource (GossipPresence *presence); +GossipPresenceState gossip_presence_get_state (GossipPresence *presence); +const gchar * gossip_presence_get_status (GossipPresence *presence); +gint gossip_presence_get_priority (GossipPresence *presence); + +void gossip_presence_set_resource (GossipPresence *presence, + const gchar *resource); +void gossip_presence_set_state (GossipPresence *presence, + GossipPresenceState state); +void gossip_presence_set_status (GossipPresence *presence, + const gchar *status); +void gossip_presence_set_priority (GossipPresence *presence, + gint priority); +gboolean gossip_presence_resource_equal (gconstpointer a, + gconstpointer b); +gint gossip_presence_sort_func (gconstpointer a, + gconstpointer b); + +const gchar * gossip_presence_state_get_default_status (GossipPresenceState state); + +G_END_DECLS + +#endif /* __GOSSIP_PRESENCE_H__ */ + diff --git a/libempathy/gossip-telepathy-group.c b/libempathy/gossip-telepathy-group.c new file mode 100644 index 00000000..4b04ac42 --- /dev/null +++ b/libempathy/gossip-telepathy-group.c @@ -0,0 +1,496 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2006 Xavier Claessens <xclaesse@gmail.com> + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include <config.h> + +#include <dbus/dbus-glib.h> +#include <libtelepathy/tp-chan.h> +#include <libtelepathy/tp-chan-iface-group-gen.h> +#include <libtelepathy/tp-constants.h> +#include <libtelepathy/tp-conn.h> + +#include "gossip-debug.h" +#include "gossip-telepathy-group.h" +#include "empathy-marshal.h" + +#define GET_PRIV(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), \ + GOSSIP_TYPE_TELEPATHY_GROUP, GossipTelepathyGroupPriv)) + +#define DEBUG_DOMAIN "TelepathyGroup" + +struct _GossipTelepathyGroupPriv { + DBusGProxy *group_iface; + TpConn *tp_conn; + TpChan *tp_chan; + gchar *group_name; +}; + +static void gossip_telepathy_group_class_init (GossipTelepathyGroupClass *klass); +static void gossip_telepathy_group_init (GossipTelepathyGroup *group); +static void telepathy_group_finalize (GObject *object); +static void telepathy_group_destroy_cb (DBusGProxy *proxy, + GossipTelepathyGroup *group); +static void telepathy_group_members_changed_cb (DBusGProxy *group_iface, + gchar *message, + GArray *added, + GArray *removed, + GArray *local_pending, + GArray *remote_pending, + guint actor, + guint reason, + GossipTelepathyGroup *group); + +enum { + MEMBERS_ADDED, + MEMBERS_REMOVED, + LOCAL_PENDING, + REMOTE_PENDING, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +G_DEFINE_TYPE (GossipTelepathyGroup, gossip_telepathy_group, G_TYPE_OBJECT); + +static void +gossip_telepathy_group_class_init (GossipTelepathyGroupClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = telepathy_group_finalize; + + signals[MEMBERS_ADDED] = + g_signal_new ("members-added", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + empathy_marshal_VOID__POINTER_UINT_UINT_STRING, + G_TYPE_NONE, + 4, G_TYPE_POINTER, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_STRING); + + signals[MEMBERS_REMOVED] = + g_signal_new ("members-removed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + empathy_marshal_VOID__POINTER_UINT_UINT_STRING, + G_TYPE_NONE, + 4, G_TYPE_POINTER, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_STRING); + + signals[LOCAL_PENDING] = + g_signal_new ("local-pending", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + empathy_marshal_VOID__POINTER_UINT_UINT_STRING, + G_TYPE_NONE, + 4, G_TYPE_POINTER, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_STRING); + + signals[REMOTE_PENDING] = + g_signal_new ("remote-pending", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + empathy_marshal_VOID__POINTER_UINT_UINT_STRING, + G_TYPE_NONE, + 4, G_TYPE_POINTER, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_STRING); + + g_type_class_add_private (object_class, sizeof (GossipTelepathyGroupPriv)); +} + +static void +gossip_telepathy_group_init (GossipTelepathyGroup *group) +{ +} + +static void +telepathy_group_finalize (GObject *object) +{ + GossipTelepathyGroupPriv *priv; + + priv = GET_PRIV (object); + + if (priv->group_iface) { + g_signal_handlers_disconnect_by_func (priv->group_iface, + telepathy_group_destroy_cb, + object); + dbus_g_proxy_disconnect_signal (priv->group_iface, "MembersChanged", + G_CALLBACK (telepathy_group_members_changed_cb), + object); + g_object_unref (priv->group_iface); + } + + if (priv->tp_conn) { + g_object_unref (priv->tp_conn); + } + + if (priv->tp_chan) { + g_object_unref (priv->tp_chan); + } + + g_free (priv->group_name); + + G_OBJECT_CLASS (gossip_telepathy_group_parent_class)->finalize (object); +} + +GossipTelepathyGroup * +gossip_telepathy_group_new (TpChan *tp_chan, + TpConn *tp_conn) +{ + GossipTelepathyGroup *group; + GossipTelepathyGroupPriv *priv; + DBusGProxy *group_iface; + + g_return_val_if_fail (TELEPATHY_IS_CHAN (tp_chan), NULL); + + group_iface = tp_chan_get_interface (tp_chan, + TELEPATHY_CHAN_IFACE_GROUP_QUARK); + g_return_val_if_fail (group_iface != NULL, NULL); + + group = g_object_new (GOSSIP_TYPE_TELEPATHY_GROUP, NULL); + priv = GET_PRIV (group); + + priv->tp_conn = g_object_ref (tp_conn); + priv->tp_chan = g_object_ref (tp_chan); + priv->group_iface = g_object_ref (group_iface); + + dbus_g_proxy_connect_signal (priv->group_iface, "MembersChanged", + G_CALLBACK (telepathy_group_members_changed_cb), + group, NULL); + g_signal_connect (group_iface, "destroy", + G_CALLBACK (telepathy_group_destroy_cb), + group); + + + return group; +} + +void +gossip_telepathy_group_add_members (GossipTelepathyGroup *group, + GArray *handles, + const gchar *message) +{ + GossipTelepathyGroupPriv *priv; + GError *error = NULL; + + g_return_if_fail (GOSSIP_IS_TELEPATHY_GROUP (group)); + g_return_if_fail (handles != NULL); + + priv = GET_PRIV (group); + + if (!tp_chan_iface_group_add_members (priv->group_iface, + handles, + message, + &error)) { + gossip_debug (DEBUG_DOMAIN, + "Failed to add members: %s", + error ? error->message : "No error given"); + g_clear_error (&error); + } +} + +void +gossip_telepathy_group_add_member (GossipTelepathyGroup *group, + guint handle, + const gchar *message) +{ + GArray *handles; + + handles = g_array_new (FALSE, FALSE, sizeof (guint)); + g_array_append_val (handles, handle); + + gossip_telepathy_group_add_members (group, handles, message); + + g_array_free (handles, TRUE); +} + +void +gossip_telepathy_group_remove_members (GossipTelepathyGroup *group, + GArray *handles, + const gchar *message) +{ + GossipTelepathyGroupPriv *priv; + GError *error = NULL; + + g_return_if_fail (GOSSIP_IS_TELEPATHY_GROUP (group)); + + priv = GET_PRIV (group); + + if (!tp_chan_iface_group_remove_members (priv->group_iface, + handles, + message, + &error)) { + gossip_debug (DEBUG_DOMAIN, + "Failed to remove members: %s", + error ? error->message : "No error given"); + g_clear_error (&error); + } +} + +void +gossip_telepathy_group_remove_member (GossipTelepathyGroup *group, + guint handle, + const gchar *message) +{ + GArray *handles; + + g_return_if_fail (GOSSIP_IS_TELEPATHY_GROUP (group)); + + handles = g_array_new (FALSE, FALSE, sizeof (guint)); + g_array_append_val (handles, handle); + + gossip_telepathy_group_remove_members (group, handles, message); + + g_array_free (handles, TRUE); +} + +GArray * +gossip_telepathy_group_get_members (GossipTelepathyGroup *group) +{ + GossipTelepathyGroupPriv *priv; + GArray *members; + GError *error = NULL; + + g_return_val_if_fail (GOSSIP_IS_TELEPATHY_GROUP (group), NULL); + + priv = GET_PRIV (group); + + if (!tp_chan_iface_group_get_members (priv->group_iface, + &members, + &error)) { + gossip_debug (DEBUG_DOMAIN, + "Couldn't get members: %s", + error ? error->message : "No error given"); + g_clear_error (&error); + return NULL; + } + + return members; +} + +void +gossip_telepathy_group_get_all_members (GossipTelepathyGroup *group, + GArray **members, + GArray **local_pending, + GArray **remote_pending) +{ + GossipTelepathyGroupPriv *priv; + GError *error = NULL; + + g_return_if_fail (GOSSIP_IS_TELEPATHY_GROUP (group)); + + priv = GET_PRIV (group); + + if (!tp_chan_iface_group_get_all_members (priv->group_iface, + members, + local_pending, + remote_pending, + &error)) { + gossip_debug (DEBUG_DOMAIN, + "Couldn't get all members: %s", + error ? error->message : "No error given"); + g_clear_error (&error); + } +} + +GPtrArray * +gossip_telepathy_group_get_local_pending_members_with_info (GossipTelepathyGroup *group) +{ + GossipTelepathyGroupPriv *priv; + GPtrArray *info = NULL; + GError *error = NULL; + + g_return_val_if_fail (GOSSIP_IS_TELEPATHY_GROUP (group), NULL); + + priv = GET_PRIV (group); + + if (!tp_chan_iface_group_get_local_pending_members_with_info (priv->group_iface, + &info, + &error)) { + gossip_debug (DEBUG_DOMAIN, + "GetLocalPendingMembersWithInfo failed: %s", + error ? error->message : "No error given"); + g_clear_error (&error); + } + + return info; +} + +static void +telepathy_group_destroy_cb (DBusGProxy *proxy, + GossipTelepathyGroup *group) +{ + GossipTelepathyGroupPriv *priv; + + priv = GET_PRIV (group); + + g_object_unref (priv->group_iface); + g_object_unref (priv->tp_conn); + g_object_unref (priv->tp_chan); + priv->group_iface = NULL; + priv->tp_chan = NULL; + priv->tp_conn = NULL; +} + +static void +telepathy_group_members_changed_cb (DBusGProxy *group_iface, + gchar *message, + GArray *added, + GArray *removed, + GArray *local_pending, + GArray *remote_pending, + guint actor, + guint reason, + GossipTelepathyGroup *group) +{ + GossipTelepathyGroupPriv *priv; + + priv = GET_PRIV (group); + + /* emit signals */ + if (added->len > 0) { + g_signal_emit (group, signals[MEMBERS_ADDED], 0, + added, actor, reason, message); + } + if (removed->len > 0) { + g_signal_emit (group, signals[MEMBERS_REMOVED], 0, + removed, actor, reason, message); + } + if (local_pending->len > 0) { + g_signal_emit (group, signals[LOCAL_PENDING], 0, + local_pending, actor, reason, message); + } + if (remote_pending->len > 0) { + g_signal_emit (group, signals[REMOTE_PENDING], 0, + remote_pending, actor, reason, message); + } +} + +const gchar * +gossip_telepathy_group_get_name (GossipTelepathyGroup *group) +{ + TelepathyHandleType handle_type; + guint channel_handle; + GArray *group_handles; + gchar **group_names; + GError *error = NULL; + + GossipTelepathyGroupPriv *priv; + + g_return_val_if_fail (GOSSIP_IS_TELEPATHY_GROUP (group), NULL); + + priv = GET_PRIV (group); + + /* Lazy initialisation */ + if (priv->group_name) { + return priv->group_name; + } + + if (!tp_chan_get_handle (DBUS_G_PROXY (priv->tp_chan), + &handle_type, + &channel_handle, + &error)) { + gossip_debug (DEBUG_DOMAIN, + "Couldn't retreive channel handle for group: %s", + error ? error->message : "No error given"); + g_clear_error (&error); + return NULL; + } + + group_handles = g_array_new (FALSE, FALSE, sizeof (gint)); + g_array_append_val (group_handles, channel_handle); + if (!tp_conn_inspect_handles (DBUS_G_PROXY (priv->tp_conn), + handle_type, + group_handles, + &group_names, + &error)) { + gossip_debug (DEBUG_DOMAIN, + "Couldn't get group name: %s", + error ? error->message : "No error given"); + g_clear_error (&error); + g_array_free (group_handles, TRUE); + return NULL; + } + + priv->group_name = *group_names; + g_array_free (group_handles, TRUE); + g_free (group_names); + + return priv->group_name; +} + +guint +gossip_telepathy_group_get_self_handle (GossipTelepathyGroup *group) +{ + GossipTelepathyGroupPriv *priv; + guint handle; + GError *error = NULL; + + g_return_val_if_fail (GOSSIP_IS_TELEPATHY_GROUP (group), 0 ); + + priv = GET_PRIV (group); + + if (!tp_chan_iface_group_get_self_handle (priv->group_iface, &handle, &error)) { + gossip_debug (DEBUG_DOMAIN, + "Failed to get self handle: %s", + error ? error->message : "No error given"); + g_clear_error (&error); + return 0; + } + + return handle; +} + +const gchar * +gossip_telepathy_group_get_object_path (GossipTelepathyGroup *group) +{ + GossipTelepathyGroupPriv *priv; + + g_return_val_if_fail (GOSSIP_IS_TELEPATHY_GROUP (group), NULL); + + priv = GET_PRIV (group); + + return dbus_g_proxy_get_path (DBUS_G_PROXY (priv->tp_chan)); +} + +gboolean +gossip_telepathy_group_is_member (GossipTelepathyGroup *group, + guint handle) +{ + GArray *members; + guint i; + gboolean found = FALSE; + + members = gossip_telepathy_group_get_members (group); + for (i = 0; i < members->len; i++) { + if (g_array_index (members, guint, i) == handle) { + found = TRUE; + break; + } + } + g_array_free (members, TRUE); + + return found; +} + diff --git a/libempathy/gossip-telepathy-group.h b/libempathy/gossip-telepathy-group.h new file mode 100644 index 00000000..9c61bdbc --- /dev/null +++ b/libempathy/gossip-telepathy-group.h @@ -0,0 +1,79 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2006 Xavier Claessens <xclaesse@gmail.com> + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __GOSSIP_TELEPATHY_GROUP_H__ +#define __GOSSIP_TELEPATHY_GROUP_H__ + +#include <glib.h> + +#include <libtelepathy/tp-chan.h> + +G_BEGIN_DECLS + +#define GOSSIP_TYPE_TELEPATHY_GROUP (gossip_telepathy_group_get_type ()) +#define GOSSIP_TELEPATHY_GROUP(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GOSSIP_TYPE_TELEPATHY_GROUP, GossipTelepathyGroup)) +#define GOSSIP_TELEPATHY_GROUP_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GOSSIP_TYPE_TELEPATHY_GROUP, GossipTelepathyGroupClass)) +#define GOSSIP_IS_TELEPATHY_GROUP(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GOSSIP_TYPE_TELEPATHY_GROUP)) +#define GOSSIP_IS_TELEPATHY_GROUP_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GOSSIP_TYPE_TELEPATHY_GROUP)) +#define GOSSIP_TELEPATHY_GROUP_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GOSSIP_TYPE_TELEPATHY_GROUP, GossipTelepathyGroupClass)) + +typedef struct _GossipTelepathyGroup GossipTelepathyGroup; +typedef struct _GossipTelepathyGroupClass GossipTelepathyGroupClass; +typedef struct _GossipTelepathyGroupPriv GossipTelepathyGroupPriv; + +struct _GossipTelepathyGroup { + GObject parent; +}; + +struct _GossipTelepathyGroupClass { + GObjectClass parent_class; +}; + +GType gossip_telepathy_group_get_type (void) G_GNUC_CONST; +GossipTelepathyGroup *gossip_telepathy_group_new (TpChan *tp_chan, + TpConn *tp_conn); +void gossip_telepathy_group_add_members (GossipTelepathyGroup *group, + GArray *handles, + const gchar *message); +void gossip_telepathy_group_add_member (GossipTelepathyGroup *group, + guint handle, + const gchar *message); +void gossip_telepathy_group_remove_members (GossipTelepathyGroup *group, + GArray *handle, + const gchar *message); +void gossip_telepathy_group_remove_member (GossipTelepathyGroup *group, + guint handle, + const gchar *message); +GArray * gossip_telepathy_group_get_members (GossipTelepathyGroup *group); +void gossip_telepathy_group_get_all_members (GossipTelepathyGroup *group, + GArray **members, + GArray **local_pending, + GArray **remote_pending); +GPtrArray * gossip_telepathy_group_get_local_pending_members_with_info + (GossipTelepathyGroup *group); +const gchar * gossip_telepathy_group_get_name (GossipTelepathyGroup *group); +guint gossip_telepathy_group_get_self_handle (GossipTelepathyGroup *group); +const gchar * gossip_telepathy_group_get_object_path (GossipTelepathyGroup *group); +gboolean gossip_telepathy_group_is_member (GossipTelepathyGroup *group, + guint handle); + +G_END_DECLS + +#endif /* __GOSSIP_TELEPATHY_GROUP_H__ */ diff --git a/libempathy/gossip-time.c b/libempathy/gossip-time.c new file mode 100644 index 00000000..a1956354 --- /dev/null +++ b/libempathy/gossip-time.c @@ -0,0 +1,124 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2003-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Richard Hult <richard@imendio.com> + */ + +#include "config.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "gossip-time.h" + +/* Note: GossipTime is always in UTC. */ + +GossipTime +gossip_time_get_current (void) +{ + return time (NULL); +} + +time_t +gossip_time_get_local_time (struct tm *tm) +{ + const gchar *timezone; + time_t t; + + timezone = g_getenv ("TZ"); + g_setenv ("TZ", "", TRUE); + + tzset (); + + t = mktime (tm); + + if (timezone) { + g_setenv ("TZ", timezone, TRUE); + } else { + g_unsetenv ("TZ"); + } + + tzset (); + + return t; +} + +/* The format is: "20021209T23:51:30" and is in UTC. 0 is returned on + * failure. The alternative format "20021209" is also accepted. + */ +GossipTime +gossip_time_parse (const gchar *str) +{ + struct tm tm; + gint year, month; + gint n_parsed; + + memset (&tm, 0, sizeof (struct tm)); + + n_parsed = sscanf (str, "%4d%2d%2dT%2d:%2d:%2d", + &year, &month, &tm.tm_mday, &tm.tm_hour, + &tm.tm_min, &tm.tm_sec); + if (n_parsed != 3 && n_parsed != 6) { + return 0; + } + + tm.tm_year = year - 1900; + tm.tm_mon = month - 1; + tm.tm_isdst = -1; + + return gossip_time_get_local_time (&tm); +} + +/* Converts the UTC timestamp to a string, also in UTC. Returns NULL on failure. */ +gchar * +gossip_time_to_string_utc (GossipTime t, + const gchar *format) +{ + gchar stamp[128]; + struct tm *tm; + + g_return_val_if_fail (format != NULL, NULL); + + tm = gmtime (&t); + if (strftime (stamp, sizeof (stamp), format, tm) == 0) { + return NULL; + } + + return g_strdup (stamp); +} + +/* Converts the UTC timestamp to a string, in local time. Returns NULL on failure. */ +gchar * +gossip_time_to_string_local (GossipTime t, + const gchar *format) +{ + gchar stamp[128]; + struct tm *tm; + + g_return_val_if_fail (format != NULL, NULL); + + tm = localtime (&t); + if (strftime (stamp, sizeof (stamp), format, tm) == 0) { + return NULL; + } + + return g_strdup (stamp); +} + diff --git a/libempathy/gossip-time.h b/libempathy/gossip-time.h new file mode 100644 index 00000000..06057aa5 --- /dev/null +++ b/libempathy/gossip-time.h @@ -0,0 +1,50 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2004 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __GOSSIP_TIME_H__ +#define __GOSSIP_TIME_H__ + +#define __USE_XOPEN +#include <time.h> + +#include <glib.h> + +G_BEGIN_DECLS + +#define GOSSIP_TIME_FORMAT_DISPLAY_SHORT "%H:%M" +#define GOSSIP_TIME_FORMAT_DISPLAY_LONG "%a %d %b %Y" + +/* Note: Always in UTC. */ +typedef long GossipTime; + +GossipTime gossip_time_get_current (void); +time_t gossip_time_get_local_time (struct tm *tm); +GossipTime gossip_time_parse (const gchar *str); +GossipTime gossip_time_parse_format (const gchar *str, + const gchar *format); +gchar *gossip_time_to_string_utc (GossipTime t, + const gchar *format); +gchar *gossip_time_to_string_local (GossipTime t, + const gchar *format); + +G_END_DECLS + +#endif /* __GOSSIP_TIME_H__ */ + diff --git a/libempathy/gossip-utils.c b/libempathy/gossip-utils.c new file mode 100644 index 00000000..24f5cd43 --- /dev/null +++ b/libempathy/gossip-utils.c @@ -0,0 +1,447 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2003-2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Richard Hult <richard@imendio.com> + * Martyn Russell <martyn@imendio.com> + */ + +#include "config.h" + +#include <string.h> +#include <time.h> +#include <sys/types.h> +#include <regex.h> + +#include <glib/gi18n.h> + +#include <libxml/uri.h> +#include <libmissioncontrol/mc-account.h> + +#include "gossip-debug.h" +#include "gossip-utils.h" +#include "gossip-paths.h" +#include "empathy-session.h" +#include "empathy-contact-manager.h" + +#define DEBUG_DOMAIN "Utils" + +static void regex_init (void); + +gchar * +gossip_substring (const gchar *str, + gint start, + gint end) +{ + return g_strndup (str + start, end - start); +} + +/* + * Regular Expression code to match urls. + */ +#define USERCHARS "-A-Za-z0-9" +#define PASSCHARS "-A-Za-z0-9,?;.:/!%$^*&~\"#'" +#define HOSTCHARS "-A-Za-z0-9" +#define PATHCHARS "-A-Za-z0-9_$.+!*(),;:@&=?/~#%" +#define SCHEME "(news:|telnet:|nntp:|file:/|https?:|ftps?:|webcal:)" +#define USER "[" USERCHARS "]+(:["PASSCHARS "]+)?" +#define URLPATH "/[" PATHCHARS "]*[^]'.}>) \t\r\n,\\\"]" + +static regex_t dingus[GOSSIP_REGEX_ALL]; + +static void +regex_init (void) +{ + static gboolean inited = FALSE; + const gchar *expression; + gint i; + + if (inited) { + return; + } + + for (i = 0; i < GOSSIP_REGEX_ALL; i++) { + switch (i) { + case GOSSIP_REGEX_AS_IS: + expression = + SCHEME "//(" USER "@)?[" HOSTCHARS ".]+" + "(:[0-9]+)?(" URLPATH ")?"; + break; + case GOSSIP_REGEX_BROWSER: + expression = + "(www|ftp)[" HOSTCHARS "]*\\.[" HOSTCHARS ".]+" + "(:[0-9]+)?(" URLPATH ")?"; + break; + case GOSSIP_REGEX_EMAIL: + expression = + "(mailto:)?[a-z0-9][a-z0-9.-]*@[a-z0-9]" + "[a-z0-9-]*(\\.[a-z0-9][a-z0-9-]*)+"; + break; + case GOSSIP_REGEX_OTHER: + expression = + "news:[-A-Z\\^_a-z{|}~!\"#$%&'()*+,./0-9;:=?`]+" + "@[" HOSTCHARS ".]+(:[0-9]+)?"; + break; + default: + /* Silence the compiler. */ + expression = NULL; + continue; + } + + memset (&dingus[i], 0, sizeof (regex_t)); + regcomp (&dingus[i], expression, REG_EXTENDED | REG_ICASE); + } + + inited = TRUE; +} + +gint +gossip_regex_match (GossipRegExType type, + const gchar *msg, + GArray *start, + GArray *end) +{ + regmatch_t matches[1]; + gint ret = 0; + gint num_matches = 0; + gint offset = 0; + gint i; + + g_return_val_if_fail (type >= 0 || type <= GOSSIP_REGEX_ALL, 0); + + regex_init (); + + while (!ret && type != GOSSIP_REGEX_ALL) { + ret = regexec (&dingus[type], msg + offset, 1, matches, 0); + if (ret == 0) { + gint s; + + num_matches++; + + s = matches[0].rm_so + offset; + offset = matches[0].rm_eo + offset; + + g_array_append_val (start, s); + g_array_append_val (end, offset); + } + } + + if (type != GOSSIP_REGEX_ALL) { + gossip_debug (DEBUG_DOMAIN, + "Found %d matches for regex type:%d", + num_matches, type); + return num_matches; + } + + /* If GOSSIP_REGEX_ALL then we run ALL regex's on the string. */ + for (i = 0; i < GOSSIP_REGEX_ALL; i++, ret = 0) { + while (!ret) { + ret = regexec (&dingus[i], msg + offset, 1, matches, 0); + if (ret == 0) { + gint s; + + num_matches++; + + s = matches[0].rm_so + offset; + offset = matches[0].rm_eo + offset; + + g_array_append_val (start, s); + g_array_append_val (end, offset); + } + } + } + + gossip_debug (DEBUG_DOMAIN, + "Found %d matches for ALL regex types", + num_matches); + + return num_matches; +} + +gint +gossip_strcasecmp (const gchar *s1, + const gchar *s2) +{ + return gossip_strncasecmp (s1, s2, -1); +} + +gint +gossip_strncasecmp (const gchar *s1, + const gchar *s2, + gsize n) +{ + gchar *u1, *u2; + gint ret_val; + + u1 = g_utf8_casefold (s1, n); + u2 = g_utf8_casefold (s2, n); + + ret_val = g_utf8_collate (u1, u2); + g_free (u1); + g_free (u2); + + return ret_val; +} + +gboolean +gossip_xml_validate (xmlDoc *doc, + const gchar *dtd_filename) +{ + gchar *path, *escaped; + xmlValidCtxt cvp; + xmlDtd *dtd; + gboolean ret; + + path = gossip_paths_get_dtd_path (dtd_filename); + + /* The list of valid chars is taken from libxml. */ + escaped = xmlURIEscapeStr (path, ":@&=+$,/?;"); + + g_free (path); + + memset (&cvp, 0, sizeof (cvp)); + dtd = xmlParseDTD (NULL, escaped); + ret = xmlValidateDtd (&cvp, doc, dtd); + + xmlFree (escaped); + xmlFreeDtd (dtd); + + return ret; +} + +xmlNodePtr +gossip_xml_node_get_child (xmlNodePtr node, + const gchar *child_name) +{ + xmlNodePtr l; + + g_return_val_if_fail (node != NULL, NULL); + g_return_val_if_fail (child_name != NULL, NULL); + + for (l = node->children; l; l = l->next) { + if (l->name && strcmp (l->name, child_name) == 0) { + return l; + } + } + + return NULL; +} + +xmlChar * +gossip_xml_node_get_child_content (xmlNodePtr node, + const gchar *child_name) +{ + xmlNodePtr l; + + g_return_val_if_fail (node != NULL, NULL); + g_return_val_if_fail (child_name != NULL, NULL); + + l = gossip_xml_node_get_child (node, child_name); + if (l) { + return xmlNodeGetContent (l); + } + + return NULL; +} + +xmlNodePtr +gossip_xml_node_find_child_prop_value (xmlNodePtr node, + const gchar *prop_name, + const gchar *prop_value) +{ + xmlNodePtr l; + xmlNodePtr found = NULL; + + g_return_val_if_fail (node != NULL, NULL); + g_return_val_if_fail (prop_name != NULL, NULL); + g_return_val_if_fail (prop_value != NULL, NULL); + + for (l = node->children; l && !found; l = l->next) { + xmlChar *prop; + + if (!xmlHasProp (l, prop_name)) { + continue; + } + + prop = xmlGetProp (l, prop_name); + if (prop && strcmp (prop, prop_value) == 0) { + found = l; + } + + xmlFree (prop); + } + + return found; +} + +GType +gossip_dbus_type_to_g_type (const gchar *dbus_type_string) +{ + if (dbus_type_string == NULL) + return G_TYPE_NONE; + + if (dbus_type_string[0] == 's') { + return G_TYPE_STRING; + } + else if (dbus_type_string[0] == 'b') { + return G_TYPE_BOOLEAN; + } + else if (dbus_type_string[0] == 'q') { + return G_TYPE_UINT; + } + else if (dbus_type_string[0] == 'n') { + return G_TYPE_INT; + } + + g_assert_not_reached (); + return G_TYPE_NONE; +} + +const gchar * +gossip_g_type_to_dbus_type (GType g_type) +{ + switch (g_type) + { + case G_TYPE_STRING: + return "s"; + case G_TYPE_BOOLEAN: + return "b"; + case G_TYPE_UINT: + return "q"; + case G_TYPE_INT: + return "n"; + default: + g_assert_not_reached (); + } + + return NULL; +} + +gchar * +gossip_g_value_to_string (const GValue *value) +{ + gchar *return_string = NULL; + GValue string_g_value = {0, }; + + g_value_init (&string_g_value, G_TYPE_STRING); + g_value_transform (value, &string_g_value); + return_string = g_value_dup_string (&string_g_value); + g_value_unset (&string_g_value); + + return return_string; +} + +GValue * +gossip_string_to_g_value (const gchar *str, GType type) +{ + GValue *g_value; + + g_value = g_new0 (GValue, 1); + g_value_init (g_value, type); + + switch (type) { + case G_TYPE_STRING: + g_value_set_string (g_value, str); + break; + case G_TYPE_BOOLEAN: + g_value_set_boolean (g_value, (str[0] == 'y' || str[0] == 'T')); + break; + case G_TYPE_UINT: + g_value_set_uint (g_value, atoi (str)); + break; + case G_TYPE_INT: + g_value_set_int (g_value, atoi (str)); + break; + default: + g_assert_not_reached (); + } + + return g_value; +} + +gboolean +gossip_g_value_equal (const GValue *value1, + const GValue *value2) +{ + GType type; + + g_return_val_if_fail (value1 != NULL, FALSE); + g_return_val_if_fail (value2 != NULL, FALSE); + + type = G_VALUE_TYPE (value1); + if (type != G_VALUE_TYPE (value2)) { + return FALSE; + } + + switch (type) + { + case G_TYPE_STRING: { + const gchar *str1; + const gchar *str2; + + str1 = g_value_get_string (value1); + str2 = g_value_get_string (value2); + return (str1 && str2 && strcmp (str1, str2) == 0) || + (G_STR_EMPTY (str1) && G_STR_EMPTY (str2)); + } + case G_TYPE_BOOLEAN: + return g_value_get_boolean (value1) == g_value_get_boolean (value2); + case G_TYPE_UINT: + return g_value_get_uint (value1) == g_value_get_uint (value2); + case G_TYPE_INT: + return g_value_get_int (value1) == g_value_get_int (value2); + default: + g_warning ("Unsupported GType in value comparaison"); + } + + return FALSE; +} + +guint +gossip_account_hash (gconstpointer key) +{ + return g_str_hash (mc_account_get_unique_name (MC_ACCOUNT (key))); +} + +gboolean +gossip_account_equal (gconstpointer a, + gconstpointer b) +{ + const gchar *name_a; + const gchar *name_b; + + name_a = mc_account_get_unique_name (MC_ACCOUNT (a)); + name_b = mc_account_get_unique_name (MC_ACCOUNT (b)); + + return g_str_equal (name_a, name_b); +} + +GossipContact * +gossip_get_own_contact_from_contact (GossipContact *contact) +{ + EmpathyContactManager *manager; + McAccount *account; + + g_return_val_if_fail (GOSSIP_IS_CONTACT (contact), NULL); + + manager = empathy_session_get_contact_manager (); + account = gossip_contact_get_account (contact); + + return empathy_contact_manager_get_own (manager, account); +} + diff --git a/libempathy/gossip-utils.h b/libempathy/gossip-utils.h new file mode 100644 index 00000000..7f0ab90e --- /dev/null +++ b/libempathy/gossip-utils.h @@ -0,0 +1,87 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2002-2005 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __GOSSIP_UTILS_H__ +#define __GOSSIP_UTILS_H__ + +#include <glib.h> +#include <glib-object.h> + +#include <libxml/parser.h> +#include <libxml/tree.h> + +#include "gossip-contact.h" + +G_BEGIN_DECLS + +#define G_STR_EMPTY(x) ((x) == NULL || (x)[0] == '\0') + +typedef enum { + GOSSIP_REGEX_AS_IS, + GOSSIP_REGEX_BROWSER, + GOSSIP_REGEX_EMAIL, + GOSSIP_REGEX_OTHER, + GOSSIP_REGEX_ALL, +} GossipRegExType; + +/* Regular expressions */ +gchar * gossip_substring (const gchar *str, + gint start, + gint end); +gint gossip_regex_match (GossipRegExType type, + const gchar *msg, + GArray *start, + GArray *end); + +/* Strings */ +gint gossip_strcasecmp (const gchar *s1, + const gchar *s2); +gint gossip_strncasecmp (const gchar *s1, + const gchar *s2, + gsize n); + +/* XML */ +gboolean gossip_xml_validate (xmlDoc *doc, + const gchar *dtd_filename); +xmlNodePtr gossip_xml_node_get_child (xmlNodePtr node, + const gchar *child_name); +xmlChar * gossip_xml_node_get_child_content (xmlNodePtr node, + const gchar *child_name); +xmlNodePtr gossip_xml_node_find_child_prop_value (xmlNodePtr node, + const gchar *prop_name, + const gchar *prop_value); + + +/* GValue/GType */ +GType gossip_dbus_type_to_g_type (const gchar *dbus_type_string); +const gchar *gossip_g_type_to_dbus_type (GType g_type); +gchar * gossip_g_value_to_string (const GValue *value); +GValue * gossip_string_to_g_value (const gchar *str, + GType type); +gboolean gossip_g_value_equal (const GValue *value1, + const GValue *value2); + +guint gossip_account_hash (gconstpointer key); +gboolean gossip_account_equal (gconstpointer a, + gconstpointer b); +GossipContact * gossip_get_own_contact_from_contact (GossipContact *contact); +G_END_DECLS + +#endif /* __GOSSIP_UTILS_H__ */ diff --git a/pixmaps/Makefile.am b/pixmaps/Makefile.am new file mode 100644 index 00000000..11df65cf --- /dev/null +++ b/pixmaps/Makefile.am @@ -0,0 +1,30 @@ +imagedir = $(datadir)/empathy + +image_DATA = \ + gossip-offline.png \ + gossip-available.png \ + gossip-busy.png \ + gossip-away.png \ + gossip-extended-away.png \ + gossip-pending.png \ + gossip-message.png \ + gossip-typing.png \ + gossip-group-message.png \ + vcard_16.png \ + vcard_48.png + +gtk_update_icon_cache = gtk-update-icon-cache -f -t $(datadir)/icons/hicolor + +install-data-hook: + @-if test -z "$(DESTDIR)"; then \ + echo "Updating Gtk icon cache."; \ + $(gtk_update_icon_cache); \ + else \ + echo "*** Icon cache not updated. After install, run this:"; \ + echo "*** $(gtk_update_icon_cache)"; \ + fi + +EXTRA_DIST = \ + $(image_DATA) + + diff --git a/pixmaps/gossip-available.png b/pixmaps/gossip-available.png Binary files differnew file mode 100644 index 00000000..c2d074f5 --- /dev/null +++ b/pixmaps/gossip-available.png diff --git a/pixmaps/gossip-away.png b/pixmaps/gossip-away.png Binary files differnew file mode 100644 index 00000000..ba9ed232 --- /dev/null +++ b/pixmaps/gossip-away.png diff --git a/pixmaps/gossip-busy.png b/pixmaps/gossip-busy.png Binary files differnew file mode 100644 index 00000000..190c5ae7 --- /dev/null +++ b/pixmaps/gossip-busy.png diff --git a/pixmaps/gossip-extended-away.png b/pixmaps/gossip-extended-away.png Binary files differnew file mode 100644 index 00000000..59dbe08f --- /dev/null +++ b/pixmaps/gossip-extended-away.png diff --git a/pixmaps/gossip-group-message.png b/pixmaps/gossip-group-message.png Binary files differnew file mode 100644 index 00000000..ed2d33b9 --- /dev/null +++ b/pixmaps/gossip-group-message.png diff --git a/pixmaps/gossip-message.png b/pixmaps/gossip-message.png Binary files differnew file mode 100644 index 00000000..84c3ceff --- /dev/null +++ b/pixmaps/gossip-message.png diff --git a/pixmaps/gossip-offline.png b/pixmaps/gossip-offline.png Binary files differnew file mode 100644 index 00000000..528eae40 --- /dev/null +++ b/pixmaps/gossip-offline.png diff --git a/pixmaps/gossip-pending.png b/pixmaps/gossip-pending.png Binary files differnew file mode 100644 index 00000000..f48de812 --- /dev/null +++ b/pixmaps/gossip-pending.png diff --git a/pixmaps/gossip-typing.png b/pixmaps/gossip-typing.png Binary files differnew file mode 100644 index 00000000..defad7b0 --- /dev/null +++ b/pixmaps/gossip-typing.png diff --git a/pixmaps/vcard_16.png b/pixmaps/vcard_16.png Binary files differnew file mode 100644 index 00000000..0dfdb778 --- /dev/null +++ b/pixmaps/vcard_16.png diff --git a/pixmaps/vcard_48.png b/pixmaps/vcard_48.png Binary files differnew file mode 100644 index 00000000..776006cd --- /dev/null +++ b/pixmaps/vcard_48.png diff --git a/po/ChangeLog b/po/ChangeLog new file mode 100644 index 00000000..703e5084 --- /dev/null +++ b/po/ChangeLog @@ -0,0 +1,3 @@ +2007-04-25 Xavier Claessens <xclaesse@gmail.com> + + * Initial version diff --git a/po/Makefile.in.in b/po/Makefile.in.in new file mode 100644 index 00000000..40c8833a --- /dev/null +++ b/po/Makefile.in.in @@ -0,0 +1,218 @@ +# Makefile for program source directory in GNU NLS utilities package. +# Copyright (C) 1995, 1996, 1997 by Ulrich Drepper <drepper@gnu.ai.mit.edu> +# +# This file file be copied and used freely without restrictions. It can +# be used in projects which are not available under the GNU Public License +# but which still want to provide support for the GNU gettext functionality. +# Please note that the actual code is *not* freely available. +# +# - Modified by Owen Taylor <otaylor@redhat.com> to use GETTEXT_PACKAGE +# instead of PACKAGE and to look for po2tbl in ./ not in intl/ +# +# - Modified by jacob berkman <jacob@ximian.com> to install +# Makefile.in.in and po2tbl.sed.in for use with glib-gettextize +# +# - Modified by Rodney Dawes <dobey@novell.com> for use with intltool +# +# We have the following line for use by intltoolize: +# INTLTOOL_MAKEFILE + +GETTEXT_PACKAGE = @GETTEXT_PACKAGE@ +PACKAGE = @PACKAGE@ +VERSION = @VERSION@ + +SHELL = /bin/sh + +srcdir = @srcdir@ +top_srcdir = @top_srcdir@ +top_builddir = @top_builddir@ +VPATH = @srcdir@ + +prefix = @prefix@ +exec_prefix = @exec_prefix@ +datadir = @datadir@ +datarootdir = @datarootdir@ +libdir = @libdir@ +DATADIRNAME = @DATADIRNAME@ +itlocaledir = $(prefix)/$(DATADIRNAME)/locale +subdir = po +install_sh = @install_sh@ +# Automake >= 1.8 provides @mkdir_p@. +# Until it can be supposed, use the safe fallback: +mkdir_p = $(install_sh) -d + +INSTALL = @INSTALL@ +INSTALL_DATA = @INSTALL_DATA@ + +GMSGFMT = @GMSGFMT@ +MSGFMT = @MSGFMT@ +XGETTEXT = @XGETTEXT@ +INTLTOOL_UPDATE = @INTLTOOL_UPDATE@ +INTLTOOL_EXTRACT = @INTLTOOL_EXTRACT@ +MSGMERGE = INTLTOOL_EXTRACT=$(INTLTOOL_EXTRACT) srcdir=$(srcdir) $(INTLTOOL_UPDATE) --gettext-package $(GETTEXT_PACKAGE) --dist +GENPOT = INTLTOOL_EXTRACT=$(INTLTOOL_EXTRACT) srcdir=$(srcdir) $(INTLTOOL_UPDATE) --gettext-package $(GETTEXT_PACKAGE) --pot + +ALL_LINGUAS = @ALL_LINGUAS@ + +PO_LINGUAS=$(shell if test -r $(srcdir)/LINGUAS; then grep -v "^\#" $(srcdir)/LINGUAS; fi) + +USER_LINGUAS=$(shell if test -n "$(LINGUAS)"; then LLINGUAS="$(LINGUAS)"; ALINGUAS="$(ALL_LINGUAS)"; for lang in $$LLINGUAS; do if test -n "`grep ^$$lang$$ $(srcdir)/LINGUAS`" -o -n "`echo $$ALINGUAS|grep ' ?$$lang ?'`"; then printf "$$lang "; fi; done; fi) + +USE_LINGUAS=$(shell if test -n "$(USER_LINGUAS)"; then LLINGUAS="$(USER_LINGUAS)"; else if test -n "$(PO_LINGUAS)"; then LLINGUAS="$(PO_LINGUAS)"; else LLINGUAS="$(ALL_LINGUAS)"; fi; fi; for lang in $$LLINGUAS; do printf "$$lang "; done) + +POFILES=$(shell LINGUAS="$(USE_LINGUAS)"; for lang in $$LINGUAS; do printf "$$lang.po "; done) + +DISTFILES = ChangeLog Makefile.in.in POTFILES.in $(POFILES) +EXTRA_DISTFILES = POTFILES.skip Makevars LINGUAS + +POTFILES = \ +# This comment gets stripped out + +CATALOGS=$(shell LINGUAS="$(USE_LINGUAS)"; for lang in $$LINGUAS; do printf "$$lang.gmo "; done) + +.SUFFIXES: +.SUFFIXES: .po .pox .gmo .mo .msg .cat + +.po.pox: + $(MAKE) $(GETTEXT_PACKAGE).pot + $(MSGMERGE) $< $(GETTEXT_PACKAGE).pot -o $*.pox + +.po.mo: + $(MSGFMT) -o $@ $< + +.po.gmo: + file=`echo $* | sed 's,.*/,,'`.gmo \ + && rm -f $$file && $(GMSGFMT) -o $$file $< + +.po.cat: + sed -f ../intl/po2msg.sed < $< > $*.msg \ + && rm -f $@ && gencat $@ $*.msg + + +all: all-@USE_NLS@ + +all-yes: $(CATALOGS) +all-no: + +$(GETTEXT_PACKAGE).pot: $(POTFILES) + $(GENPOT) + +install: install-data +install-data: install-data-@USE_NLS@ +install-data-no: all +install-data-yes: all + $(mkdir_p) $(DESTDIR)$(itlocaledir) + linguas="$(USE_LINGUAS)"; \ + for lang in $$linguas; do \ + dir=$(DESTDIR)$(itlocaledir)/$$lang/LC_MESSAGES; \ + $(mkdir_p) $$dir; \ + if test -r $$lang.gmo; then \ + $(INSTALL_DATA) $$lang.gmo $$dir/$(GETTEXT_PACKAGE).mo; \ + echo "installing $$lang.gmo as $$dir/$(GETTEXT_PACKAGE).mo"; \ + else \ + $(INSTALL_DATA) $(srcdir)/$$lang.gmo $$dir/$(GETTEXT_PACKAGE).mo; \ + echo "installing $(srcdir)/$$lang.gmo as" \ + "$$dir/$(GETTEXT_PACKAGE).mo"; \ + fi; \ + if test -r $$lang.gmo.m; then \ + $(INSTALL_DATA) $$lang.gmo.m $$dir/$(GETTEXT_PACKAGE).mo.m; \ + echo "installing $$lang.gmo.m as $$dir/$(GETTEXT_PACKAGE).mo.m"; \ + else \ + if test -r $(srcdir)/$$lang.gmo.m ; then \ + $(INSTALL_DATA) $(srcdir)/$$lang.gmo.m \ + $$dir/$(GETTEXT_PACKAGE).mo.m; \ + echo "installing $(srcdir)/$$lang.gmo.m as" \ + "$$dir/$(GETTEXT_PACKAGE).mo.m"; \ + else \ + true; \ + fi; \ + fi; \ + done + +# Empty stubs to satisfy archaic automake needs +dvi info tags TAGS ID: + +# Define this as empty until I found a useful application. +installcheck: + +uninstall: + linguas="$(USE_LINGUAS)"; \ + for lang in $$linguas; do \ + rm -f $(DESTDIR)$(itlocaledir)/$$lang/LC_MESSAGES/$(GETTEXT_PACKAGE).mo; \ + rm -f $(DESTDIR)$(itlocaledir)/$$lang/LC_MESSAGES/$(GETTEXT_PACKAGE).mo.m; \ + done + +check: all $(GETTEXT_PACKAGE).pot + rm -f missing notexist + srcdir=$(srcdir) $(INTLTOOL_UPDATE) -m + if [ -r missing -o -r notexist ]; then \ + exit 1; \ + fi + +mostlyclean: + rm -f *.pox $(GETTEXT_PACKAGE).pot *.old.po cat-id-tbl.tmp + rm -f .intltool-merge-cache + +clean: mostlyclean + +distclean: clean + rm -f Makefile Makefile.in POTFILES stamp-it + rm -f *.mo *.msg *.cat *.cat.m *.gmo + +maintainer-clean: distclean + @echo "This command is intended for maintainers to use;" + @echo "it deletes files that may require special tools to rebuild." + rm -f Makefile.in.in + +distdir = ../$(PACKAGE)-$(VERSION)/$(subdir) +dist distdir: $(DISTFILES) + dists="$(DISTFILES)"; \ + extra_dists="$(EXTRA_DISTFILES)"; \ + for file in $$extra_dists; do \ + test -f $(srcdir)/$$file && dists="$$dists $(srcdir)/$$file"; \ + done; \ + for file in $$dists; do \ + test -f $$file || file="$(srcdir)/$$file"; \ + ln $$file $(distdir) 2> /dev/null \ + || cp -p $$file $(distdir); \ + done + +update-po: Makefile + $(MAKE) $(GETTEXT_PACKAGE).pot + tmpdir=`pwd`; \ + linguas="$(USE_LINGUAS)"; \ + for lang in $$linguas; do \ + echo "$$lang:"; \ + result="`$(MSGMERGE) -o $$tmpdir/$$lang.new.po $$lang`"; \ + if $$result; then \ + if cmp $(srcdir)/$$lang.po $$tmpdir/$$lang.new.po >/dev/null 2>&1; then \ + rm -f $$tmpdir/$$lang.new.po; \ + else \ + if mv -f $$tmpdir/$$lang.new.po $$lang.po; then \ + :; \ + else \ + echo "msgmerge for $$lang.po failed: cannot move $$tmpdir/$$lang.new.po to $$lang.po" 1>&2; \ + rm -f $$tmpdir/$$lang.new.po; \ + exit 1; \ + fi; \ + fi; \ + else \ + echo "msgmerge for $$lang.gmo failed!"; \ + rm -f $$tmpdir/$$lang.new.po; \ + fi; \ + done + +Makefile POTFILES: stamp-it + @if test ! -f $@; then \ + rm -f stamp-it; \ + $(MAKE) stamp-it; \ + fi + +stamp-it: Makefile.in.in $(top_builddir)/config.status POTFILES.in + cd $(top_builddir) \ + && CONFIG_FILES=$(subdir)/Makefile.in CONFIG_HEADERS= CONFIG_LINKS= \ + $(SHELL) ./config.status + +# Tell versions [3.59,3.63) of GNU make not to export all variables. +# Otherwise a system limit (for SysV at least) may be exceeded. +.NOEXPORT: diff --git a/po/POTFILES.in b/po/POTFILES.in new file mode 100644 index 00000000..3b7c299a --- /dev/null +++ b/po/POTFILES.in @@ -0,0 +1,20 @@ +# List of source files containing translatable strings. +# Please keep this file sorted alphabetically. + + +libempathy/gossip-contact.c +libempathy/gossip-presence.c + +libempathy-gtk/empathy.schemas.in +libempathy-gtk/empathy-chat.glade +libempathy-gtk/empathy-accounts.glade +libempathy-gtk/gossip-accounts-dialog.c +libempathy-gtk/gossip-account-widget-generic.c +libempathy-gtk/gossip-chat.c +libempathy-gtk/gossip-chat-view.c +libempathy-gtk/gossip-chat-window.c +libempathy-gtk/gossip-contact-list.c +libempathy-gtk/gossip-preferences.c +libempathy-gtk/gossip-private-chat.c +libempathy-gtk/gossip-theme-manager.c + diff --git a/profiles/Makefile.am b/profiles/Makefile.am new file mode 100644 index 00000000..2536260b --- /dev/null +++ b/profiles/Makefile.am @@ -0,0 +1,8 @@ +profiledir = $(datadir)/mission-control/profiles + +profile_DATA = \ + jabber.profile \ + msn.profile \ + gtalk.profile + +EXTRA_DIST = $(profile_DATA) diff --git a/profiles/gtalk.profile b/profiles/gtalk.profile new file mode 100644 index 00000000..c70bfa84 --- /dev/null +++ b/profiles/gtalk.profile @@ -0,0 +1,14 @@ +[Profile] +Manager=gabble +Protocol=jabber +DisplayName=Google Talk +IconName = im-jabber +ConfigurationUI = jabber +DefaultAccountDomain = gmail.com, googlemail.com +Default-server = talk.google.com +Default-port = 5223 +Default-old-ssl = 1 +Default-fallback-conference-server = conference.jabber.org +Default-randomize-resource = 1 +Default-ignore-ssl-errors = 1 +SupportsInvisible = 0 diff --git a/profiles/jabber.profile b/profiles/jabber.profile new file mode 100644 index 00000000..811132cd --- /dev/null +++ b/profiles/jabber.profile @@ -0,0 +1,8 @@ +[Profile] +Manager=gabble +Protocol=jabber +DisplayName=Jabber +IconName = im-jabber +ConfigurationUI = jabber +DefaultAccountDomain = jabber.org +SupportsInvisible = 0 diff --git a/profiles/msn.profile b/profiles/msn.profile new file mode 100644 index 00000000..599204c1 --- /dev/null +++ b/profiles/msn.profile @@ -0,0 +1,7 @@ +[Profile] +Manager=butterfly +Protocol=msn +DisplayName=MSN +IconName = im-msn +ConfigurationUI = msn +SupportsInvisible = 1 |