From 40c0d1ba5434b5c5d4f8b7ff6fedd1add5c03ce6 Mon Sep 17 00:00:00 2001 From: Xavier Claessens Date: Wed, 25 Apr 2007 21:59:20 +0000 Subject: [darcs-to-svn @ initial import] svn path=/trunk/; revision=2 --- AUTHORS | 4 + CONTRIBUTORS | 17 + COPYING | 340 ++++ ChangeLog | 4 + MAINTAINERS | 1 + Makefile.am | 24 + NEWS | 0 README | 15 + TODO | 20 + accounts/Makefile.am | 19 + accounts/empathy-accounts-main.c | 59 + acinclude.m4 | 65 + autogen.sh | 174 ++ chat/Makefile.am | 40 + chat/empathy-chat-main.c | 93 + chat/empathy-chat.chandler | 5 + chat/org.gnome.Empathy.Chat.service.in | 3 + configure.ac | 75 + contact-list/Makefile.am | 20 + contact-list/empathy-contact-list-main.c | 94 + launcher/Makefile.am | 12 + launcher/empathy-launcher.c | 47 + libempathy-gtk/Makefile.am | 68 + libempathy-gtk/empathy-accounts.glade | 843 +++++++++ libempathy-gtk/empathy-chat.glade | 700 +++++++ libempathy-gtk/empathy.schemas.in | 237 +++ libempathy-gtk/gossip-account-widget-generic.c | 309 +++ libempathy-gtk/gossip-account-widget-generic.h | 38 + libempathy-gtk/gossip-accounts-dialog.c | 1032 ++++++++++ libempathy-gtk/gossip-accounts-dialog.h | 34 + libempathy-gtk/gossip-cell-renderer-expander.c | 482 +++++ libempathy-gtk/gossip-cell-renderer-expander.h | 59 + libempathy-gtk/gossip-cell-renderer-text.c | 368 ++++ libempathy-gtk/gossip-cell-renderer-text.h | 56 + libempathy-gtk/gossip-chat-manager.c | 327 ++++ libempathy-gtk/gossip-chat-manager.h | 65 + libempathy-gtk/gossip-chat-manager.loT | 7 + libempathy-gtk/gossip-chat-view.c | 2151 +++++++++++++++++++++ libempathy-gtk/gossip-chat-view.h | 134 ++ libempathy-gtk/gossip-chat-window.c | 1894 +++++++++++++++++++ libempathy-gtk/gossip-chat-window.h | 74 + libempathy-gtk/gossip-chat.c | 1295 +++++++++++++ libempathy-gtk/gossip-chat.h | 139 ++ libempathy-gtk/gossip-contact-groups.c | 286 +++ libempathy-gtk/gossip-contact-groups.dtd | 17 + libempathy-gtk/gossip-contact-groups.h | 38 + libempathy-gtk/gossip-contact-list.c | 2419 ++++++++++++++++++++++++ libempathy-gtk/gossip-contact-list.h | 72 + libempathy-gtk/gossip-preferences.c | 885 +++++++++ libempathy-gtk/gossip-preferences.h | 55 + libempathy-gtk/gossip-private-chat.c | 495 +++++ libempathy-gtk/gossip-private-chat.h | 62 + libempathy-gtk/gossip-profile-chooser.c | 102 + libempathy-gtk/gossip-profile-chooser.h | 34 + libempathy-gtk/gossip-spell.c | 457 +++++ libempathy-gtk/gossip-spell.h | 39 + libempathy-gtk/gossip-stock.c | 105 + libempathy-gtk/gossip-stock.h | 60 + libempathy-gtk/gossip-theme-manager.c | 1045 ++++++++++ libempathy-gtk/gossip-theme-manager.h | 64 + libempathy-gtk/gossip-ui-utils.c | 1360 +++++++++++++ libempathy-gtk/gossip-ui-utils.h | 115 ++ libempathy/Makefile.am | 54 + libempathy/empathy-chandler.c | 150 ++ libempathy/empathy-chandler.h | 53 + libempathy/empathy-chandler.xml | 13 + libempathy/empathy-contact-list.c | 1793 ++++++++++++++++++ libempathy/empathy-contact-list.h | 76 + libempathy/empathy-contact-manager.c | 550 ++++++ libempathy/empathy-contact-manager.h | 77 + libempathy/empathy-marshal-main.c | 2 + libempathy/empathy-marshal.list | 3 + libempathy/empathy-session.c | 161 ++ libempathy/empathy-session.h | 38 + libempathy/empathy-tp-chat.c | 474 +++++ libempathy/empathy-tp-chat.h | 75 + libempathy/gossip-avatar.c | 86 + libempathy/gossip-avatar.h | 48 + libempathy/gossip-conf.c | 382 ++++ libempathy/gossip-conf.h | 87 + libempathy/gossip-contact.c | 815 ++++++++ libempathy/gossip-contact.h | 102 + libempathy/gossip-debug.c | 92 + libempathy/gossip-debug.h | 53 + libempathy/gossip-message.c | 365 ++++ libempathy/gossip-message.h | 75 + libempathy/gossip-paths.c | 51 + libempathy/gossip-paths.h | 36 + libempathy/gossip-presence.c | 441 +++++ libempathy/gossip-presence.h | 84 + libempathy/gossip-telepathy-group.c | 496 +++++ libempathy/gossip-telepathy-group.h | 79 + libempathy/gossip-time.c | 124 ++ libempathy/gossip-time.h | 50 + libempathy/gossip-utils.c | 447 +++++ libempathy/gossip-utils.h | 87 + pixmaps/Makefile.am | 30 + pixmaps/gossip-available.png | Bin 0 -> 741 bytes pixmaps/gossip-away.png | Bin 0 -> 609 bytes pixmaps/gossip-busy.png | Bin 0 -> 888 bytes pixmaps/gossip-extended-away.png | Bin 0 -> 773 bytes pixmaps/gossip-group-message.png | Bin 0 -> 764 bytes pixmaps/gossip-message.png | Bin 0 -> 594 bytes pixmaps/gossip-offline.png | Bin 0 -> 524 bytes pixmaps/gossip-pending.png | Bin 0 -> 786 bytes pixmaps/gossip-typing.png | Bin 0 -> 820 bytes pixmaps/vcard_16.png | Bin 0 -> 411 bytes pixmaps/vcard_48.png | Bin 0 -> 3155 bytes po/ChangeLog | 3 + po/Makefile.in.in | 218 +++ po/POTFILES.in | 20 + profiles/Makefile.am | 8 + profiles/gtalk.profile | 14 + profiles/jabber.profile | 8 + profiles/msn.profile | 7 + 115 files changed, 26980 insertions(+) create mode 100644 AUTHORS create mode 100644 CONTRIBUTORS create mode 100644 COPYING create mode 100644 ChangeLog create mode 100644 MAINTAINERS create mode 100644 Makefile.am create mode 100644 NEWS create mode 100644 README create mode 100644 TODO create mode 100644 accounts/Makefile.am create mode 100644 accounts/empathy-accounts-main.c create mode 100644 acinclude.m4 create mode 100644 autogen.sh create mode 100644 chat/Makefile.am create mode 100644 chat/empathy-chat-main.c create mode 100644 chat/empathy-chat.chandler create mode 100644 chat/org.gnome.Empathy.Chat.service.in create mode 100644 configure.ac create mode 100644 contact-list/Makefile.am create mode 100644 contact-list/empathy-contact-list-main.c create mode 100644 launcher/Makefile.am create mode 100644 launcher/empathy-launcher.c create mode 100644 libempathy-gtk/Makefile.am create mode 100644 libempathy-gtk/empathy-accounts.glade create mode 100644 libempathy-gtk/empathy-chat.glade create mode 100644 libempathy-gtk/empathy.schemas.in create mode 100644 libempathy-gtk/gossip-account-widget-generic.c create mode 100644 libempathy-gtk/gossip-account-widget-generic.h create mode 100644 libempathy-gtk/gossip-accounts-dialog.c create mode 100644 libempathy-gtk/gossip-accounts-dialog.h create mode 100644 libempathy-gtk/gossip-cell-renderer-expander.c create mode 100644 libempathy-gtk/gossip-cell-renderer-expander.h create mode 100644 libempathy-gtk/gossip-cell-renderer-text.c create mode 100644 libempathy-gtk/gossip-cell-renderer-text.h create mode 100644 libempathy-gtk/gossip-chat-manager.c create mode 100644 libempathy-gtk/gossip-chat-manager.h create mode 100644 libempathy-gtk/gossip-chat-manager.loT create mode 100644 libempathy-gtk/gossip-chat-view.c create mode 100644 libempathy-gtk/gossip-chat-view.h create mode 100644 libempathy-gtk/gossip-chat-window.c create mode 100644 libempathy-gtk/gossip-chat-window.h create mode 100644 libempathy-gtk/gossip-chat.c create mode 100644 libempathy-gtk/gossip-chat.h create mode 100644 libempathy-gtk/gossip-contact-groups.c create mode 100644 libempathy-gtk/gossip-contact-groups.dtd create mode 100644 libempathy-gtk/gossip-contact-groups.h create mode 100644 libempathy-gtk/gossip-contact-list.c create mode 100644 libempathy-gtk/gossip-contact-list.h create mode 100644 libempathy-gtk/gossip-preferences.c create mode 100644 libempathy-gtk/gossip-preferences.h create mode 100644 libempathy-gtk/gossip-private-chat.c create mode 100644 libempathy-gtk/gossip-private-chat.h create mode 100644 libempathy-gtk/gossip-profile-chooser.c create mode 100644 libempathy-gtk/gossip-profile-chooser.h create mode 100644 libempathy-gtk/gossip-spell.c create mode 100644 libempathy-gtk/gossip-spell.h create mode 100644 libempathy-gtk/gossip-stock.c create mode 100644 libempathy-gtk/gossip-stock.h create mode 100644 libempathy-gtk/gossip-theme-manager.c create mode 100644 libempathy-gtk/gossip-theme-manager.h create mode 100644 libempathy-gtk/gossip-ui-utils.c create mode 100644 libempathy-gtk/gossip-ui-utils.h create mode 100644 libempathy/Makefile.am create mode 100644 libempathy/empathy-chandler.c create mode 100644 libempathy/empathy-chandler.h create mode 100644 libempathy/empathy-chandler.xml create mode 100644 libempathy/empathy-contact-list.c create mode 100644 libempathy/empathy-contact-list.h create mode 100644 libempathy/empathy-contact-manager.c create mode 100644 libempathy/empathy-contact-manager.h create mode 100644 libempathy/empathy-marshal-main.c create mode 100644 libempathy/empathy-marshal.list create mode 100644 libempathy/empathy-session.c create mode 100644 libempathy/empathy-session.h create mode 100644 libempathy/empathy-tp-chat.c create mode 100644 libempathy/empathy-tp-chat.h create mode 100644 libempathy/gossip-avatar.c create mode 100644 libempathy/gossip-avatar.h create mode 100644 libempathy/gossip-conf.c create mode 100644 libempathy/gossip-conf.h create mode 100644 libempathy/gossip-contact.c create mode 100644 libempathy/gossip-contact.h create mode 100644 libempathy/gossip-debug.c create mode 100644 libempathy/gossip-debug.h create mode 100644 libempathy/gossip-message.c create mode 100644 libempathy/gossip-message.h create mode 100644 libempathy/gossip-paths.c create mode 100644 libempathy/gossip-paths.h create mode 100644 libempathy/gossip-presence.c create mode 100644 libempathy/gossip-presence.h create mode 100644 libempathy/gossip-telepathy-group.c create mode 100644 libempathy/gossip-telepathy-group.h create mode 100644 libempathy/gossip-time.c create mode 100644 libempathy/gossip-time.h create mode 100644 libempathy/gossip-utils.c create mode 100644 libempathy/gossip-utils.h create mode 100644 pixmaps/Makefile.am create mode 100644 pixmaps/gossip-available.png create mode 100644 pixmaps/gossip-away.png create mode 100644 pixmaps/gossip-busy.png create mode 100644 pixmaps/gossip-extended-away.png create mode 100644 pixmaps/gossip-group-message.png create mode 100644 pixmaps/gossip-message.png create mode 100644 pixmaps/gossip-offline.png create mode 100644 pixmaps/gossip-pending.png create mode 100644 pixmaps/gossip-typing.png create mode 100644 pixmaps/vcard_16.png create mode 100644 pixmaps/vcard_48.png create mode 100644 po/ChangeLog create mode 100644 po/Makefile.in.in create mode 100644 po/POTFILES.in create mode 100644 profiles/Makefile.am create mode 100644 profiles/gtalk.profile create mode 100644 profiles/jabber.profile create mode 100644 profiles/msn.profile diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 000000000..2847eaa37 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,4 @@ +Mikael Hallendal +Richard Hult +Martyn Russell +Xavier Claessens diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 000000000..a9a8cd1fb --- /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 000000000..d60c31a97 --- /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. + + + Copyright (C) + + 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. + + , 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 000000000..86b8105ed --- /dev/null +++ b/ChangeLog @@ -0,0 +1,4 @@ +2006-03-16 Xavier Claessens + + * Initial version + diff --git a/MAINTAINERS b/MAINTAINERS new file mode 100644 index 000000000..422a3ed6c --- /dev/null +++ b/MAINTAINERS @@ -0,0 +1 @@ +Xavier Claessens diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 000000000..bf4cebf43 --- /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' diff --git a/NEWS b/NEWS new file mode 100644 index 000000000..e69de29bb diff --git a/README b/README new file mode 100644 index 000000000..ef16e2c7c --- /dev/null +++ b/README @@ -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. + diff --git a/TODO b/TODO new file mode 100644 index 000000000..9fe1f1c50 --- /dev/null +++ b/TODO @@ -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 000000000..589cae3a3 --- /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 000000000..ec30b8338 --- /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 + * + * 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 + +#include +#include + +#include +#include + +#include +#include + +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 000000000..3120d8620 --- /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 000000000..1b93b756f --- /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 000000000..d8ee78a07 --- /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 000000000..f27944994 --- /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 + * + * 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 + +#include + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#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 000000000..4cfe75b01 --- /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 000000000..6a794e4b8 --- /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 000000000..42a162120 --- /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 000000000..de6786368 --- /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 000000000..2f4328480 --- /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 + * + * 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 + +#include + +#include +#include + +#include +#include +#include +#include +#include + +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 000000000..b0662539d --- /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 000000000..44b390fb3 --- /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 + * + * 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 + +#include + +#include + +#include +#include + +#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 000000000..162715684 --- /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 000000000..a7bb391d2 --- /dev/null +++ b/libempathy-gtk/empathy-accounts.glade @@ -0,0 +1,843 @@ + + + + + + 5 + Accounts + GDK_WINDOW_TYPE_HINT_DIALOG + False + + + True + 2 + + + True + 6 + 18 + + + True + 6 + + + True + True + GTK_POLICY_AUTOMATIC + GTK_POLICY_AUTOMATIC + GTK_SHADOW_IN + + + 200 + True + True + False + + + + + + + True + 6 + + + True + True + gtk-connect + True + + + False + False + + + + + True + 6 + True + + + True + True + gtk-add + True + + + + + True + True + gtk-remove + True + + + 1 + + + + + 1 + + + + + False + False + 1 + + + + + False + False + + + + + 415 + True + 18 + + + True + 18 + + + True + 0 + GTK_SHADOW_NONE + + + True + 6 + 20 + + + True + 6 + + + True + 2 + 2 + 6 + 6 + + + True + 0 + Jabber + + + + + + + + True + 0 + Imendio + 0 + + + 1 + 2 + + + + + + True + 0 + gtk-cut + 6 + + + 1 + 2 + 2 + GTK_FILL + GTK_FILL + + + + + + + + + + + True + <b>Account</b> + True + + + label_item + + + + + False + False + + + + + True + 0 + GTK_SHADOW_NONE + + + True + 6 + 20 + + + + + + + + True + <b>Settings</b> + True + + + label_item + + + + + 1 + + + + + + + 0 + GTK_SHADOW_NONE + + + True + 6 + 20 + + + True + 12 + + + True + 2 + 2 + 6 + 6 + + + + + + True + 0 + _Name: + True + entry_name + + + 1 + 2 + GTK_FILL + + + + + + True + 0 + _Type: + True + + + GTK_FILL + + + + + + True + 6 + + + True + True + A unique name for this account to identify it personally to you. + * + + + + + 1 + 2 + 1 + 2 + GTK_FILL + GTK_FILL + + + + + 1 + + + + + True + 6 + + + True + True + gtk-cancel + True + + + False + False + GTK_PACK_END + 1 + + + + + True + False + True + + + True + 0 + 0 + + + True + 2 + + + True + gtk-new + + + False + False + + + + + True + Cr_eate! + True + + + False + False + 1 + + + + + + + + + False + False + GTK_PACK_END + + + + + False + False + GTK_PACK_END + 1 + + + + + + + + + True + <b>New Account</b> + True + + + label_item + + + + + False + 1 + + + + + 0 + GTK_SHADOW_NONE + + + True + 6 + 12 + + + True + 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. + True + True + + + + + + + True + <b>No Account Selected</b> + True + + + label_item + + + + + False + False + 2 + + + + + 1 + + + + + 1 + + + + + True + GTK_BUTTONBOX_END + + + True + True + True + gtk-close + True + -6 + + + + + False + GTK_PACK_END + + + + + + + True + jabber account settings + False + + + True + 6 + 3 + 12 + 6 + + + True + 0 + Login I_D: + True + entry_id + + + GTK_FILL + + + + + + True + 0 + Pass_word: + True + entry_password + + + 1 + 2 + GTK_FILL + + + + + + True + 0 + Reso_urce: + True + entry_resource + + + 2 + 3 + GTK_FILL + + + + + + True + 0 + _Server: + True + entry_server + + + 3 + 4 + GTK_FILL + + + + + + True + 0 + _Port: + True + entry_port + + + 4 + 5 + GTK_FILL + + + + + + True + True + * + + + 1 + 3 + 2 + 3 + + + + + + True + True + * + + + 1 + 3 + 3 + 4 + + + + + + True + True + * + + + 1 + 3 + 4 + 5 + + + + + + True + True + Use encryption (SS_L) + True + True + + + 3 + 5 + 6 + GTK_FILL + + + + + + True + 2 + + + True + True + False + * + + + + + True + True + Forget password and clear the entry. + + + True + gtk-clear + 1 + + + + + False + False + 1 + + + + + 1 + 2 + 1 + 2 + GTK_FILL + GTK_FILL + + + + + True + False + True + C_hange + True + + + 2 + 3 + 1 + 2 + GTK_FILL + + + + + + True + True + * + + + 1 + 2 + + + + + + True + True + R_egister + True + + + 2 + 3 + GTK_FILL + + + + + + + + True + msn account settings + False + + + True + 4 + 2 + 12 + 6 + + + True + 0 + Login I_D: + True + entry_id + + + GTK_FILL + + + + + + True + 0 + Pass_word: + True + entry_password + + + 1 + 2 + GTK_FILL + + + + + + True + 2 + + + True + True + False + * + + + + + True + True + Forget password and clear the entry. + + + True + gtk-clear + 1 + + + + + False + False + 1 + + + + + 1 + 2 + 1 + 2 + GTK_FILL + GTK_FILL + + + + + True + True + * + + + 1 + 2 + + + + + + True + 0 + _Server: + True + entry_server + + + 2 + 3 + GTK_FILL + + + + + + True + True + * + + + 1 + 2 + 2 + 3 + + + + + + True + 0 + _Port: + True + entry_port + + + 3 + 4 + GTK_FILL + + + + + + True + True + * + + + 1 + 2 + 3 + 4 + + + + + + + diff --git a/libempathy-gtk/empathy-chat.glade b/libempathy-gtk/empathy-chat.glade new file mode 100644 index 000000000..e8bd47cfe --- /dev/null +++ b/libempathy-gtk/empathy-chat.glade @@ -0,0 +1,700 @@ + + + + + + + + Chat + GTK_WINDOW_TOPLEVEL + GTK_WIN_POS_NONE + False + 350 + 250 + True + False + True + False + False + GDK_WINDOW_TYPE_HINT_NORMAL + GDK_GRAVITY_NORTH_WEST + True + False + + + + 4 + True + False + 3 + + + + True + True + GTK_POLICY_AUTOMATIC + GTK_POLICY_ALWAYS + GTK_SHADOW_IN + GTK_CORNER_TOP_LEFT + + + + + + + 0 + True + True + + + + + + True + True + GTK_POLICY_NEVER + GTK_POLICY_NEVER + GTK_SHADOW_IN + GTK_CORNER_TOP_LEFT + + + + + + + 0 + False + False + + + + + + + + Chat + GTK_WINDOW_TOPLEVEL + GTK_WIN_POS_NONE + False + 350 + 250 + True + False + True + False + False + GDK_WINDOW_TYPE_HINT_NORMAL + GDK_GRAVITY_NORTH_WEST + True + False + + + + True + False + 0 + + + + True + GTK_PACK_DIRECTION_LTR + GTK_PACK_DIRECTION_LTR + + + + True + _Conversation + True + + + + + + + True + C_lear + True + + + + True + gtk-clear + 1 + 0.5 + 0.5 + 0 + 0 + + + + + + + + True + Insert _Smiley + True + + + + + + True + + + + + + True + _View Previous Conversations + True + + + + + True + gtk-justify-left + 1 + 0.5 + 0.5 + 0 + 0 + + + + + + + + True + + + + + + _Add Contact... + True + + + + True + gtk-add + 1 + 0.5 + 0.5 + 0 + 0 + + + + + + + + True + Contact Infor_mation + True + + + + True + gtk-info + 1 + 0.5 + 0.5 + 0 + 0 + + + + + + + + True + + + + + + True + _Close + True + + + + + True + gtk-close + 1 + 0.5 + 0.5 + 0 + 0 + + + + + + + + + + + + _Room + True + + + + + + + True + Change _Topic... + True + + + + + + True + + + + + + True + Join _New... + True + + + + True + gtk-new + 1 + 0.5 + 0.5 + 0 + 0 + + + + + + + + True + In_vite... + True + + + + + + True + + + + + + True + _Add To Favorites + True + + + + True + gtk-add + 1 + 0.5 + 0.5 + 0 + 0 + + + + + + + + True + + + + + + True + _Show Contacts + True + True + + + + + + + + + + + True + _Edit + True + + + + + + + True + Cu_t + True + + + + + True + gtk-cut + 1 + 0.5 + 0.5 + 0 + 0 + + + + + + + + True + _Copy + True + + + + + True + gtk-copy + 1 + 0.5 + 0.5 + 0 + 0 + + + + + + + + True + _Paste + True + + + + + True + gtk-paste + 1 + 0.5 + 0.5 + 0 + 0 + + + + + + + + + + + + True + _Tabs + True + + + + + + + True + _Previous Tab + True + + + + + + + True + _Next Tab + True + + + + + + + True + + + + + + True + Move Tab _Left + True + + + + + + True + Move Tab _Right + True + + + + + + True + _Detach Tab + True + + + + + + + + + 0 + False + False + + + + + + + + + + + + 5 + Invite + GTK_WINDOW_TOPLEVEL + GTK_WIN_POS_CENTER_ON_PARENT + True + 275 + 225 + True + False + True + False + False + GDK_WINDOW_TYPE_HINT_DIALOG + GDK_GRAVITY_NORTH_WEST + True + False + False + + + + True + False + 0 + + + + True + GTK_BUTTONBOX_END + + + + True + True + True + gtk-cancel + True + GTK_RELIEF_NORMAL + True + -6 + + + + + + True + False + True + True + True + In_vite + True + GTK_RELIEF_NORMAL + True + -5 + + + + + 0 + False + True + GTK_PACK_END + + + + + + 5 + True + False + 18 + + + + True + False + 6 + + + + True + Select who would you like to invite: + False + True + GTK_JUSTIFY_LEFT + True + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + False + False + + + + + + True + True + GTK_POLICY_AUTOMATIC + GTK_POLICY_AUTOMATIC + GTK_SHADOW_IN + GTK_CORNER_TOP_LEFT + + + + True + True + False + False + False + True + False + False + False + + + + + 0 + True + True + + + + + 0 + True + True + + + + + + True + False + 6 + + + + True + Invitation _message: + True + True + GTK_JUSTIFY_LEFT + True + False + 0 + 0.5 + 0 + 0 + entry + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + False + False + + + + + + True + True + True + True + 0 + You have been invited to join a chat conference. + True + * + True + 40 + + + 0 + False + False + + + + + 0 + False + False + + + + + 0 + True + True + + + + + + + diff --git a/libempathy-gtk/empathy.schemas.in b/libempathy-gtk/empathy.schemas.in new file mode 100644 index 000000000..243e6ce27 --- /dev/null +++ b/libempathy-gtk/empathy.schemas.in @@ -0,0 +1,237 @@ + + + + + /schemas/apps/empathy/ui/show_offline + /apps/empathy/ui/show_offline + empathy + bool + false + + Show offline contacts + + Whether or not to show contacts that are offline in the contact list. + + + + + + /schemas/apps/empathy/ui/show_avatars + /apps/empathy/ui/show_avatars + empathy + bool + true + + Show avatars + + Whether or not to show avatars for contacts in the contact + list and chat windows. + + + + + + /schemas/apps/empathy/ui/compact_contact_list + /apps/empathy/ui/compact_contact_list + empathy + bool + false + + Compact contact list + + Whether to show the contact list in compact mode or not. + + + + + + /schemas/apps/empathy/ui/main_window_hidden + /apps/empathy/ui/main_window_hidden + empathy + bool + false + + Hide main window + + Hide the main window. + + + + + + /schemas/apps/empathy/ui/avatar_directory + /apps/empathy/ui/avatar_directory + empathy + string + + + Default directory to select an avatar image from + + The last directory that an avatar image was chosen from. + + + + + + /schemas/apps/empathy/notifications/play_sounds + /apps/empathy/notifications/play_sounds + empathy + bool + true + + Use notification sounds + + Whether or not to play a sound when messages arrive. + + + + + + /schemas/apps/empathy/notifications/sound_when_away + /apps/empathy/notifications/sound_when_away + empathy + bool + false + + Enable sound when away + + Whether or not to play sounds when away. + + + + + + /schemas/apps/empathy/notifications/sound_when_busy + /apps/empathy/notifications/sound_when_busy + empathy + bool + true + + Enable sound when busy + + Whether or not to play sounds when busy. + + + + + + /schemas/apps/empathy/notifications/popup_when_contact_available + /apps/empathy/notifications/popup_when_contact_available + empathy + bool + true + + Enable popup when contact is available + + Whether or not to show a popup when a contact becomes available. + + + + + + /schemas/apps/empathy/ui/separate_chat_windows + /apps/empathy/ui/separate_chat_windows + empathy + bool + false + + Open new chats in separate windows + + Always open a separate chat window for new chats. + + + + + + /schemas/apps/empathy/conversation/graphical_smileys + /apps/empathy/conversation/graphical_smileys + empathy + bool + true + + Use graphical smileys + + Whether or not to convert smileys into graphical images in + conversations. + + + + + + /schemas/apps/empathy/conversation/theme + /apps/empathy/conversation/theme + empathy + string + classic + + Chat window theme + + The theme that is used to display the conversation in chat windows. + + + + + + /schemas/apps/empathy/conversation/theme_chat_room + /apps/empathy/conversation/theme_chat_room + empathy + bool + true + + Use theme for chat rooms + + Whether to use the theme for chat rooms or not. + + + + + + /schemas/apps/empathy/conversation/spell_checker_languages + /apps/empathy/conversation/spell_checker_languages + empathy + string + en + + Spell checking languages + + Comma separated list of spell checker languages to use (e.g. en, fr, nl). + + + + + + /schemas/apps/empathy/conversation/spell_checker_enabled + /apps/empathy/conversation/spell_checker_enabled + empathy + bool + true + + Enable spell checker + + Whether or not to check words typed against the languages you + want to check with. + + + + + + /schemas/apps/empathy/hints/close_main_window + /apps/empathy/hints/close_main_window + empathy + bool + true + + Show hint about closing the main window + + Whether or not to show the message dialog about closing the + main window with the 'x' button in the title bar. + + + + + + + + + + diff --git a/libempathy-gtk/gossip-account-widget-generic.c b/libempathy-gtk/gossip-account-widget-generic.c new file mode 100644 index 000000000..7f7846397 --- /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 + * Martyn Russell + */ + +#include + +#include + +#include +#include + +#include +#include + +#include + +#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 000000000..1a3db63ae --- /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 + * Martyn Russell + */ + +#ifndef __GOSSIP_ACCOUNT_WIDGET_GENERIC_H__ +#define __GOSSIP_ACCOUNT_WIDGET_GENERIC_H__ + +#include + +#include + +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 000000000..43d974778 --- /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 + * Xavier Claessens + */ + +#include + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#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), + _("No Account Selected")); + 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), + _("No Accounts Configured")); + 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 000000000..b6b0593c7 --- /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 + */ + +#ifndef __GOSSIP_ACCOUNTS_DIALOG_H__ +#define __GOSSIP_ACCOUNTS_DIALOG_H__ + +#include + +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 000000000..e116ace7b --- /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 + */ + +/* 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 + +#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 000000000..7df474668 --- /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 + */ + +#ifndef __GOSSIP_CELL_RENDERER_EXPANDER_H__ +#define __GOSSIP_CELL_RENDERER_EXPANDER_H__ + +#include + +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 000000000..2b54eedf5 --- /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 + */ + +#include "config.h" + +#include + +#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 000000000..9b5a413b7 --- /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 + */ + +#ifndef __GOSSIP_CELL_RENDERER_TEXT_H__ +#define __GOSSIP_CELL_RENDERER_TEXT_H__ + +#include + +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 000000000..86cd0ea3f --- /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 + */ + +#include "config.h" + +#include + +#include + +#include +#include +#include +#include +#include +#include + +#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 000000000..7500b594a --- /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 + */ + +#ifndef __GOSSIP_CHAT_MANAGER_H__ +#define __GOSSIP_CHAT_MANAGER_H__ + +#include + +#include + +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 000000000..3d0d3ab0d --- /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 000000000..cc6482ee8 --- /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 + * Richard Hult + * Martyn Russell + */ + +#include "config.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#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 000000000..2a7b11472 --- /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 + * Richard Hult + * Martyn Russell + */ + +#ifndef __GOSSIP_CHAT_VIEW_H__ +#define __GOSSIP_CHAT_VIEW_H__ + +#include +#include + +#include +#include + +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 000000000..da305e60b --- /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 + * Richard Hult + * Martyn Russell + * Geert-Jan Van den Bogaerde + */ + +#include "config.h" + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#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 000000000..8cf582d8a --- /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 + * Richard Hult + * Martyn Russell + * Geert-Jan Van den Bogaerde + */ + +#ifndef __GOSSIP_CHAT_WINDOW_H__ +#define __GOSSIP_CHAT_WINDOW_H__ + +#include + +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 000000000..616b3abe3 --- /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 + * Richard Hult + * Martyn Russell + * Geert-Jan Van den Bogaerde + */ + +#include "config.h" + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#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 000000000..9a0a0c317 --- /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 + * Richard Hult + * Martyn Russell + * Geert-Jan Van den Bogaerde + */ + +#ifndef __GOSSIP_CHAT_H__ +#define __GOSSIP_CHAT_H__ + +#include + +#include +#include +#include + +#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 000000000..8a6afda11 --- /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 + */ + +#include "config.h" + +#include +#include +#include + +#include +#include + +#include +#include + +#include +#include + +#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 000000000..689220f0e --- /dev/null +++ b/libempathy-gtk/gossip-contact-groups.dtd @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/libempathy-gtk/gossip-contact-groups.h b/libempathy-gtk/gossip-contact-groups.h new file mode 100644 index 000000000..88bbdc0cb --- /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 + */ + +#ifndef __GOSSIP_CONTACT_GROUPS_H__ +#define __GOSSIP_CONTACT_GROUPS_H__ + +G_BEGIN_DECLS + +#include + +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 000000000..6e1573d8f --- /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 + * Martyn Russell + */ + +#include "config.h" + +#include + +#include +#include +#include + +#include + +#include +#include +#include + +#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"), "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 = + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + ""; + +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 000000000..b5947b947 --- /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 + * Martyn Russell + */ + +#ifndef __GOSSIP_CONTACT_LIST_H__ +#define __GOSSIP_CONTACT_LIST_H__ + +#include + +#include + +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 000000000..2f127a515 --- /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 + * Richard Hult + * Martyn Russell + */ + +#include "config.h" + +#include + +#include +#include +#include + +#include + +#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 000000000..428d0fc72 --- /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 + * Richard Hult + * Martyn Russell + */ + +#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 000000000..41a7f5946 --- /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 + * Richard Hult + * Martyn Russell + * Geert-Jan Van den Bogaerde + */ + +#include "config.h" + +#include + +#include +#include +#include + +#include + +#include +#include +//#include + +#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 000000000..48741696d --- /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 + * Richard Hult + * Martyn Russell + * Geert-Jan Van den Bogaerde + */ + +#ifndef __GOSSIP_PRIVATE_CHAT_H__ +#define __GOSSIP_PRIVATE_CHAT_H__ + +#include +#include + +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 000000000..15f2fbb34 --- /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 + * + * 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 + +#include +#include + +#include + +#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 000000000..e608db7e1 --- /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 + */ + +#ifndef __GOSSIP_PROTOCOL_CHOOSER_H__ +#define __GOSSIP_PROTOCOL_CHOOSER_H__ + +#include + +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 000000000..db06e9f1d --- /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 + * Richard Hult + */ + +#include "config.h" + +#include +#include + +#include + +#ifdef HAVE_ASPELL +#include +#endif + +#include +#include + +#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 000000000..f2d841b3a --- /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 + * Richard Hult + */ + +#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 000000000..f43949ee4 --- /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 + * Richard Hult + * Martyn Russell + */ + +#include "config.h" + +#include + +#include + +#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 000000000..412aceef1 --- /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 + * Richard Hult + * Martyn Russell + */ + +#ifndef __GOSSIP_STOCK_H__ +#define __GOSSIP_STOCK_H__ + +#include +#include + +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 000000000..6d5905e5a --- /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 + */ + +#include "config.h" + +#include + +#include +#include + +#include +#include + +#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 000000000..bb7e3cf70 --- /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 + */ + +#ifndef __GOSSIP_THEME_MANAGER_H__ +#define __GOSSIP_THEME_MANAGER_H__ + +#include + +#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 000000000..e902012f1 --- /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 + * Richard Hult + * Martyn Russell + * + * Part of this file is copied from GtkSourceView (gtksourceiter.c): + * Paolo Maggi + * Jeroen Zwartepoorte + */ + +#include + +#include +#include +#include +#include + +#include + +#include +#include + +#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 000000000..581587a73 --- /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 + * Richard Hult + * Martyn Russell + * + * 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 +#include + +#include +#include + +#include +#include +#include + +#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 000000000..30d40c595 --- /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 000000000..adc415a93 --- /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 + * + * 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 + +#include + +#include +#include +#include + +#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 000000000..ca271613c --- /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 + * + * 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 + +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 000000000..0fb264e5f --- /dev/null +++ b/libempathy/empathy-chandler.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/libempathy/empathy-contact-list.c b/libempathy/empathy-contact-list.c new file mode 100644 index 000000000..2cc486de9 --- /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 + * + * 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 + +#include + +#include +#include +#include +#include +#include +#include +#include + +#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 000000000..db8b36e39 --- /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 + * + * 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 +#include + +#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 000000000..15fb82eed --- /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 + * + * 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 + +#include + +#include + +#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 000000000..ca6cb3fe5 --- /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 + * + * 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 + +#include + +#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 000000000..f3ab95f54 --- /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 000000000..6035b9451 --- /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 000000000..fefda7694 --- /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 + * + * 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 + +#include + +#include + +#include + +#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 000000000..0d3a2dce1 --- /dev/null +++ b/libempathy/empathy-session.h @@ -0,0 +1,38 @@ +#include /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2007 Xavier Claessens + * + * 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 + +#include +#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 000000000..ad00711d9 --- /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 + * + * 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 + +#include +#include +#include + +#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 000000000..c64bbd738 --- /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 + * + * 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 + +#include + +#include + +#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 000000000..5c17a5176 --- /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 + * Xavier Claessens + */ + +#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 000000000..44fa9aba3 --- /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 + * + * 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 + +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 000000000..9625a700d --- /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 + */ + +#include "config.h" + +#include + +#include + +#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 000000000..35fdfb902 --- /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 + +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 000000000..6950c89b7 --- /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 + * Martyn Russell + */ + +#include "config.h" + +#include + +#include + +#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 000000000..92caee9ea --- /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 + +#include + +#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 000000000..a6bbeea2a --- /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 + */ + +#include "config.h" + +#include +#include + +#include +#include + +/* 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 000000000..39dae0f1c --- /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 + +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 000000000..85889e7da --- /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 + */ + +#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 000000000..8defba7e7 --- /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 + +#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 000000000..aa97bdcc7 --- /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 + */ + +#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 000000000..e00a33e28 --- /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 + +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 000000000..e41ae5548 --- /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 + */ + +#include "config.h" + +#include + +#include + +#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 000000000..64a0b8ec3 --- /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 + +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 000000000..4b04ac42f --- /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 + * + * 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 + +#include +#include +#include +#include +#include + +#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 000000000..9c61bdbc4 --- /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 + * + * 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 + +#include + +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 000000000..a1956354e --- /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 + */ + +#include "config.h" + +#include +#include +#include + +#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 000000000..06057aa52 --- /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 + +#include + +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 000000000..24f5cd432 --- /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 + * Martyn Russell + */ + +#include "config.h" + +#include +#include +#include +#include + +#include + +#include +#include + +#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 000000000..7f0ab90ef --- /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 +#include + +#include +#include + +#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 000000000..11df65cf9 --- /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 new file mode 100644 index 000000000..c2d074f50 Binary files /dev/null and b/pixmaps/gossip-available.png differ diff --git a/pixmaps/gossip-away.png b/pixmaps/gossip-away.png new file mode 100644 index 000000000..ba9ed232a Binary files /dev/null and b/pixmaps/gossip-away.png differ diff --git a/pixmaps/gossip-busy.png b/pixmaps/gossip-busy.png new file mode 100644 index 000000000..190c5ae70 Binary files /dev/null and b/pixmaps/gossip-busy.png differ diff --git a/pixmaps/gossip-extended-away.png b/pixmaps/gossip-extended-away.png new file mode 100644 index 000000000..59dbe08f8 Binary files /dev/null and b/pixmaps/gossip-extended-away.png differ diff --git a/pixmaps/gossip-group-message.png b/pixmaps/gossip-group-message.png new file mode 100644 index 000000000..ed2d33b9f Binary files /dev/null and b/pixmaps/gossip-group-message.png differ diff --git a/pixmaps/gossip-message.png b/pixmaps/gossip-message.png new file mode 100644 index 000000000..84c3cefff Binary files /dev/null and b/pixmaps/gossip-message.png differ diff --git a/pixmaps/gossip-offline.png b/pixmaps/gossip-offline.png new file mode 100644 index 000000000..528eae409 Binary files /dev/null and b/pixmaps/gossip-offline.png differ diff --git a/pixmaps/gossip-pending.png b/pixmaps/gossip-pending.png new file mode 100644 index 000000000..f48de8127 Binary files /dev/null and b/pixmaps/gossip-pending.png differ diff --git a/pixmaps/gossip-typing.png b/pixmaps/gossip-typing.png new file mode 100644 index 000000000..defad7b08 Binary files /dev/null and b/pixmaps/gossip-typing.png differ diff --git a/pixmaps/vcard_16.png b/pixmaps/vcard_16.png new file mode 100644 index 000000000..0dfdb7780 Binary files /dev/null and b/pixmaps/vcard_16.png differ diff --git a/pixmaps/vcard_48.png b/pixmaps/vcard_48.png new file mode 100644 index 000000000..776006cd8 Binary files /dev/null and b/pixmaps/vcard_48.png differ diff --git a/po/ChangeLog b/po/ChangeLog new file mode 100644 index 000000000..703e5084d --- /dev/null +++ b/po/ChangeLog @@ -0,0 +1,3 @@ +2007-04-25 Xavier Claessens + + * Initial version diff --git a/po/Makefile.in.in b/po/Makefile.in.in new file mode 100644 index 000000000..40c8833ac --- /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 +# +# 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 to use GETTEXT_PACKAGE +# instead of PACKAGE and to look for po2tbl in ./ not in intl/ +# +# - Modified by jacob berkman to install +# Makefile.in.in and po2tbl.sed.in for use with glib-gettextize +# +# - Modified by Rodney Dawes 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 000000000..3b7c299a4 --- /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 000000000..2536260bb --- /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 000000000..c70bfa840 --- /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 000000000..811132cdf --- /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 000000000..599204c1b --- /dev/null +++ b/profiles/msn.profile @@ -0,0 +1,7 @@ +[Profile] +Manager=butterfly +Protocol=msn +DisplayName=MSN +IconName = im-msn +ConfigurationUI = msn +SupportsInvisible = 1 -- cgit v1.2.3