aboutsummaryrefslogtreecommitdiffstats
path: root/NEWS
Commit message (Expand)AuthorAgeFilesLines
* *** empty log message ***Rodrigo Moya2003-07-011-18/+0
* Updated NEWSRodrigo Moya2003-06-301-0/+6
* Updated NEWSRodrigo Moya2003-06-301-0/+12
* oops, move the addressbook NEWS to the right versionChris Toshok2003-06-261-7/+7
* add addressbook NEWSChris Toshok2003-06-261-0/+7
* Update.Ettore Perazzoli2003-06-261-22/+49
* updatedJeffrey Stedfast2003-06-261-0/+5
* More NEWSRodrigo Moya2003-06-261-0/+3
* updatedJeffrey Stedfast2003-06-261-0/+2
* Updated NEWSRodrigo Moya2003-06-241-0/+5
* Add #43585.Hans Petter2003-06-241-1/+3
* Some more mail updatesMichael Zucci2003-06-201-1/+9
* updatedJeffrey Stedfast2003-06-201-0/+1
* *** empty log message ***Rodrigo Moya2003-06-181-0/+4
* updatedJeffrey Stedfast2003-06-181-0/+1
* updatedJeffrey Stedfast2003-06-181-0/+1
* Updated for current mail stuff.Not Zed2003-06-171-0/+40
* 1.4.0!Ettore Perazzoli2003-06-031-9/+40
* updated mailer fixesJeffrey Stedfast2003-06-031-0/+12
* *** empty log message ***Rodrigo Moya2003-06-021-0/+1
* Updated NEWSRodrigo Moya2003-05-291-0/+9
* 1.3.92.Ettore Perazzoli2003-05-231-1/+1
* updated NEWSRadek Doulik2003-05-231-0/+1
* updateJP Rosevear2003-05-231-1/+3
* Ooops, forgot to add the updated translations.Ettore Perazzoli2003-05-221-0/+18
* 1.3.91.Ettore Perazzoli2003-05-221-1/+1
* 41234Rodrigo Moya2003-05-221-0/+1
* update for preference changes.Larry Ewing2003-05-221-0/+4
* updatedJeffrey Stedfast2003-05-211-0/+3
* Update.Ettore Perazzoli2003-05-211-75/+115
* updatedJeffrey Stedfast2003-05-211-0/+3
* #42056 - Accelerators in meeting selector not working/looks bad (Hans Petter)Hans Petter2003-05-201-0/+1
* more newsRodrigo Moya2003-05-201-0/+1
* more ab stuffChris Toshok2003-05-201-0/+3
* more addressbook fooChris Toshok2003-05-201-0/+11
* add some HIG bugsChris Toshok2003-05-191-0/+5
* add some more ab fixesChris Toshok2003-05-191-1/+2
* Updated with calendar fixesRodrigo Moya2003-05-191-0/+4
* Updated for mail/ camel/ filter/ and e-util/Not Zed2003-05-191-0/+46
* Updated NEWSRodrigo Moya2003-05-181-0/+1
* add more ab fixesChris Toshok2003-05-161-0/+3
* Updated NEWSRodrigo Moya2003-05-161-0/+1
* Updated NEWSRodrigo Moya2003-05-151-0/+1
* add more ab fixesChris Toshok2003-05-151-1/+6
* Updated NEWSRodrigo Moya2003-05-141-0/+9
* add some e-text fixesChris Toshok2003-05-141-0/+4
* add #42780Chris Toshok2003-05-141-0/+2
* more ab fooChris Toshok2003-05-141-0/+1
* add some more addressbook news.Chris Toshok2003-05-121-0/+9
* move 41910 to the version where it's really fixedChris Toshok2003-05-071-1/+1
* Added list of updated translations.Ettore Perazzoli2003-05-021-0/+12
* add blurbs for some addressbook fixesChris Toshok2003-04-301-0/+11
* Update.Ettore Perazzoli2003-04-301-33/+75
* add blurb about gilbert's fixChris Toshok2003-04-301-0/+1
* Update calendar news.JP Rosevear2003-04-301-0/+22
* add blurb about 41910Chris Toshok2003-04-301-0/+1
* updatedJeffrey Stedfast2003-04-301-0/+1
* updatedJeffrey Stedfast2003-04-301-0/+2
* Updated for mailer fixesJeffrey Stedfast2003-04-301-0/+37
* add blurbs for 41843 and 41779Chris Toshok2003-04-281-0/+2
* add more newsChris Toshok2003-04-241-1/+8
* add 40694 and 40954Chris Toshok2003-04-201-1/+14
* The return of 1.3.2.Ettore Perazzoli2003-04-121-7/+8
* updated NEWSJeffrey Stedfast2003-04-121-0/+5
* More updates.JP Rosevear2003-04-121-2/+2
* Update for 1.3.2JP Rosevear2003-04-121-0/+10
* Sync for 1.3.2.Ettore Perazzoli2003-04-101-151/+217
* add 40133Chris Toshok2003-04-101-0/+1
* add 40727 fixChris Toshok2003-04-091-0/+1
* add 39763 fixChris Toshok2003-04-081-0/+1
* add blurb of addressbook fix for 7103Chris Toshok2003-04-081-0/+1
* more addressbook stuff.Chris Toshok2003-04-081-1/+3
* #35926 fixed.Hans Petter2003-04-081-0/+2
* more addressbook stuffChris Toshok2003-04-081-2/+3
* add 40841 blurbChris Toshok2003-04-081-0/+2
* add blurb about sun exporterChris Toshok2003-04-081-0/+2
* Remove a stray word causing ungrammaticality.Hans Petter2003-04-071-1/+1
* Add NEWS items for calendar.Hans Petter2003-04-071-0/+50
* add addressbook stuffChris Toshok2003-04-071-0/+46
* Updated for mail/camel/composer/filter for 1.3.2Not Zed2003-04-071-0/+95
* Up to 1.3.1.99.Ettore Perazzoli2003-03-061-580/+2
* Sync for 1.1.90.Ettore Perazzoli2002-10-291-4/+0
* corrected for mailerJeffrey Stedfast2002-10-291-3/+4
* note the selectnames changeDan Winship2002-10-291-0/+6
* Update with Addressbook and Mail news.Ettore Perazzoli2002-10-291-5/+42
* Update.Ettore Perazzoli2002-10-281-21/+36
* Updeate with lame entry, too sleepy to do it well right nowLarry Ewing2002-10-281-0/+6
* Update.JP Rosevear2002-10-261-0/+12
* Update for calendar.JP Rosevear2002-10-261-0/+29
* Sync for 1.1.2.Ettore Perazzoli2002-10-081-12/+32
* More fixups.Ettore Perazzoli2002-10-081-137/+148
* Add fixer's name to info.JP Rosevear2002-10-081-19/+19
* Remove a conflict tag.Mike Kestner2002-10-081-1/+0
* GAL news.Mike Kestner2002-10-081-0/+8
* Oops, remove redundancy.JP Rosevear2002-10-081-7/+0
* Update.JP Rosevear2002-10-081-0/+43
* Update.Ettore Perazzoli2002-10-081-52/+61
* updated news for mailerJeffrey Stedfast2002-10-081-0/+59
* Update.Ettore Perazzoli2002-10-081-2/+72
* Sync for 1.1.1.Ettore Perazzoli2002-09-101-20/+28
* update with some gtkhtml features.Larry Ewing2002-09-071-0/+14
* more NEWSChris Toshok2002-09-071-0/+2
* more contacts newsChris Toshok2002-09-071-1/+21
* NEWSIain Holmes2002-09-071-0/+16
* Update.JP Rosevear2002-09-061-1/+26
* Updated NEWSRodrigo Moya2002-09-061-1/+17
* updated NEWSMichael Zucci2002-09-061-2/+49
* updatedJeffrey Stedfast2002-09-061-6/+7
* Updated with the new 1.2 features (unfinished).Ettore Perazzoli2002-09-061-2371/+82
* Use 1 instead of zero as the minimum value for the repetitions spin buttonFederico Mena Quintero2002-01-261-0/+2
* add blurb about 17355Chris Toshok2001-12-191-0/+1
* Redone with the bug #s from Bugzilla.Ettore Perazzoli2001-11-151-20/+75
* Calendar/tasks NEWS.Federico Mena Quintero2001-11-151-0/+35
* Small updateJP Rosevear2001-11-061-0/+2
* Updated.Ettore Perazzoli2001-11-061-72/+78
* add more NEWSChris Toshok2001-11-011-0/+11
* updateJP Rosevear2001-11-011-0/+14
* NEWSIain Holmes2001-10-311-0/+3
* oops. Federico already updated it.Damon Chaplin2001-10-311-12/+0
* updatedDamon Chaplin2001-10-311-0/+10
* Updated mailer NEWS.Jeffrey Stedfast2001-10-311-0/+29
* My mailer news.02001-10-311-0/+44
* NEWSIain Holmes2001-10-311-6/+18
* Calendar NEWS.Federico Mena Quintero2001-10-311-0/+45
* Added news.Jon Trowbridge2001-10-301-0/+37
* Added my addressbook changes.Christopher James Lahey2001-10-301-0/+8
* *** empty log message ***Rodrigo Moya2001-10-301-0/+4
* Updated news.Jon Trowbridge2001-10-221-0/+29
* More NEWSRodrigo Moya2001-10-201-0/+2
* Calendar news - FedericoFederico Mena Quintero2001-10-191-2/+6
* more newsChris Toshok2001-10-171-0/+4
* Wrote Addressbook news.Christopher James Lahey2001-10-171-0/+7
* Updated NEWSRodrigo Moya2001-10-171-0/+10
* Added info about the shell, plus some minor fixes for consistency.Ettore Perazzoli2001-10-101-24/+40
* Updated.Christopher James Lahey2001-10-101-0/+8
* updated NEWSJeffrey Stedfast2001-10-101-0/+13
* Calendar NEWS.Federico Mena Quintero2001-10-101-2/+14
* Updated for beta 6.92001-10-101-0/+20
* *** empty log message ***Damon Chaplin2001-10-101-0/+4
* That was the day that wasIain Holmes2001-10-101-0/+6
* Added my news.Jon Trowbridge2001-10-101-5/+21
* *** empty log message ***Rodrigo Moya2001-10-091-0/+9
* Added more news.Christopher James Lahey2001-10-021-1/+12
* NEWSIain Holmes2001-10-021-0/+3
* NEWSIain Holmes2001-10-021-0/+19
* Updated.Ettore Perazzoli2001-10-021-13/+29
* News.Jon Trowbridge2001-10-021-0/+18
* *** empty log message ***Damon Chaplin2001-10-021-0/+2
* Updates.JP Rosevear2001-10-021-0/+10
* updated NEWSJeffrey Stedfast2001-10-021-0/+5
* updated NEWS for mailerJeffrey Stedfast2001-10-021-0/+15
* Let's start the calendar hype - FedericoFederico Mena Quintero2001-09-271-0/+15
* updated NEWSJeffrey Stedfast2001-09-221-0/+42
* Updated.12001-09-221-0/+46
* UpdateJP Rosevear2001-09-221-0/+2
* updated calendar & tasks stuffDamon Chaplin2001-09-221-0/+19
* Added mailer news I forgot earlier.Jon Trowbridge2001-09-221-2/+9
* Updated.Ettore Perazzoli2001-09-221-15/+17
* Update.Ettore Perazzoli2001-09-221-25/+52
* Added more NEWS items here.Christopher James Lahey2001-09-221-1/+18
* NEWSIain Holmes2001-09-221-0/+25
* Added some news for Ettore.Jon Trowbridge2001-09-221-0/+19
* *** empty log message ***Rodrigo Moya2001-09-211-0/+2
* updated NEWS for 0.14Jeffrey Stedfast2001-09-211-0/+16
* Added.Ettore Perazzoli2001-08-221-5/+27
* add LDAP server dialog to addressbook NEWS.Chris Toshok2001-08-221-0/+2
* Updated NEWS for addressbook.Christopher James Lahey2001-08-221-0/+56
* fixesJeffrey Stedfast2001-08-221-1/+0
* It's more NEWSIain Holmes2001-08-221-0/+2
* Ummm....It's the NEWSIain Holmes2001-08-221-0/+7
* NEWS - FedericoFederico Mena Quintero2001-08-221-0/+46
* Updated some more.Peter Williams2001-08-221-0/+16
* Updated.Jeffrey Stedfast2001-08-221-0/+29
* More cleanup.Ettore Perazzoli2001-08-011-5/+5
* NEWSIain Holmes2001-08-011-0/+12
* Clean up and re-format a bit.Ettore Perazzoli2001-08-011-68/+72
* 0.12 NEWS for the mailerPeter Williams2001-08-011-0/+70
* Calendar NEWS - FedericoFederico Mena Quintero2001-08-011-1/+23
* Added info about the shell changes and slightly changed the formatting forEttore Perazzoli2001-08-011-19/+49
* Added some NEWS calendar stuffRodrigo Moya2001-07-311-0/+6
* Updated NEWS for addressbook.Christopher James Lahey2001-07-311-0/+32
* Updated NEWSMichael Zucci2001-07-311-0/+4
* more updatesJP Rosevear2001-07-141-14/+50
* WiddleIain Holmes2001-07-141-1/+4
* oops, forgot some stuffJeffrey Stedfast2001-07-141-2/+8
* Typo fixPeter Williams2001-07-141-1/+1
* Merged mine and Peter's entries.Jeffrey Stedfast2001-07-141-10/+38
* 0.11 news for peterwPeter Williams2001-07-141-0/+27
* Ummmm, it's the NEWS, take a guessIain Holmes2001-07-141-0/+9
* Start new entryJP Rosevear2001-07-131-0/+144
* pulled from the 0.9 release.Jeffrey Stedfast2001-03-191-25/+114
* install conduit iconsJP Rosevear2001-03-031-0/+3
* Updated NEWS from 0.9 - FedericoFederico Mena Quintero2001-02-281-0/+64
* added note about better reply editingRadek Doulik2000-12-151-0/+3
* Update with the code name of the 0.8 release.Ettore Perazzoli2000-12-151-2/+2
* Added a bit of addressbook NEWS.Christopher James Lahey2000-12-151-2/+4
* Update.Jeffrey Stedfast2000-12-151-1/+9
* What I can remember doing. :)Iain Holmes2000-12-151-0/+14
* News - FedericoFederico Mena Quintero2000-12-151-1/+51
* UpdateJP Rosevear2000-12-151-1/+14
* Updated for the shell, and reformatted the part about the mailer aEttore Perazzoli2000-12-141-32/+85
* add mailer features since 0.6 (except filter stuff)Dan Winship2000-12-141-1/+80
* Added last names.Chris Lahey2000-10-201-6/+7
* Updated NEWS for addressbook and ETable.Christopher James Lahey2000-10-201-12/+33
* add mail newsDan Winship2000-10-201-0/+50
* re-commit Ettore's and JPR's 0.6 NEWS, in the right placeDan Winship2000-10-201-0/+29
* pull 0.5.1 NEWS back over to HEADDan Winship2000-10-201-0/+41
* Some minor formatting changes.Ettore Perazzoli2000-09-131-35/+37
* Pilot stuffJP Rosevear2000-09-131-1/+4
* Pilot stuffJP Rosevear2000-09-131-0/+3
* Updated a little bit.Ettore Perazzoli2000-09-131-5/+10
* Added Sent/Outbox feature descriptionsJeffrey Stedfast2000-09-131-0/+5
* add most (but not all) 0.5 Mailer featuresDan Winship2000-09-131-0/+49
* Added 0.5 changes for ETable and Addressbook.Christopher James Lahey2000-09-121-0/+26
* Ensure that the child's allocation height is >= 1.Federico Mena Quintero2000-08-311-0/+12
* add a caveat about the POP keep-on-server optionDan Winship2000-08-141-3/+4
* Fix Ettore's grammar :)Dan Winship2000-08-141-2/+2
* Update.Ettore Perazzoli2000-08-141-14/+53
* Add 0.4 mail/calendar newsDan Winship2000-08-141-4/+84
* Update and get rid of <A0>s.Ettore Perazzoli2000-07-281-5/+7
* Small fixes.Ettore Perazzoli2000-07-281-3/+3
* 0.3 "Jelly Fish".Ettore Perazzoli2000-07-281-32/+74
* add mailer 0.3 featuresDan Winship2000-07-271-0/+48
* Updated.Ettore Perazzoli2000-07-111-0/+6
* add mailer newsDan Winship2000-07-111-0/+105
* Updated with the addressbook stuff.Ettore Perazzoli2000-07-111-33/+39
* Calendar news - FedericoFederico Mena Quintero2000-07-111-0/+30
* more small typo fixes.Michael Meeks1999-11-261-1/+1
* sgmlized Giao's doc about virtual folders.bertrand1999-06-031-0/+8
* Initial revisionBertrand Guiheneuf1999-04-181-0/+0
ref='#n2415'>2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 2427 2428 2429 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442 2443 2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 2503 2504 2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559 2560 2561 2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 2588 2589 2590 2591 2592 2593 2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631 2632 2633 2634 2635 2636 2637 2638 2639 2640 2641 2642 2643 2644 2645 2646 2647 2648 2649 2650 2651 2652 2653 2654 2655 2656 2657 2658 2659 2660 2661 2662 2663 2664 2665 2666 2667 2668 2669 2670 2671 2672 2673 2674 2675 2676 2677 2678 2679 2680 2681 2682 2683 2684 2685 2686 2687 2688 2689 2690 2691 2692 2693 2694 2695 2696 2697 2698 2699 2700 2701 2702 2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 2715 2716 2717 2718 2719 2720 2721 2722 2723 2724 2725 2726 2727 2728 2729 2730 2731 2732 2733 2734 2735 2736 2737 2738 2739 2740 2741 2742 2743 2744 2745 2746 2747 2748 2749 2750 2751 2752 2753 2754 2755 2756 2757 2758 2759 2760 2761 2762 2763 2764 2765 2766 2767 2768 2769 2770 2771 2772 2773 2774 2775 2776 2777 2778 2779 2780 2781 2782 2783 2784 2785 2786 2787 2788 2789 2790 2791 2792 2793 2794 2795 2796 2797 2798 2799 2800 2801 2802 2803 2804 2805 2806 2807 2808 2809 2810 2811 2812 2813 2814 2815 2816 2817 2818 2819 2820 2821 2822 2823 2824 2825 2826 2827 2828 2829 2830 2831 2832 2833 2834 2835 2836 2837 2838 2839 2840 2841 2842 2843 2844 2845 2846 2847 2848 2849 2850 2851 2852 2853 2854 2855 2856 2857 2858 2859 2860 2861 2862 2863 2864 2865 2866 2867 2868 2869 2870 2871 2872 2873 2874 2875 2876 2877 2878 2879 2880 2881 2882 2883 2884 2885 2886 2887 2888 2889 2890 2891 2892 2893 2894 2895 2896 2897 2898 2899 2900 2901 2902 2903 2904 2905 2906 2907 2908 2909 2910 2911 2912 2913 2914 2915 2916 2917 2918 2919 2920 2921 2922 2923 2924 2925 2926 2927 2928 2929 2930 2931 2932 2933 2934 2935 2936 2937 2938 2939 2940 2941 2942 2943 2944 2945 2946 2947 2948 2949 2950 2951 2952 2953 2954 2955 2956 2957 2958 2959 2960 2961 2962 2963 2964 2965 2966 2967 2968 2969 2970 2971 2972 2973 2974 2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 2986 2987 2988 2989 2990 2991 2992 2993 2994 2995 2996 2997 2998 2999 3000 3001 3002 3003 3004 3005 3006 3007 3008 3009 3010 3011 3012 3013 3014 3015 3016 3017 3018 3019 3020 3021 3022 3023 3024 3025 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 3036 3037 3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 3050 3051 3052 3053 3054 3055 3056 3057 3058 3059 3060 3061 3062 3063 3064 3065 3066 3067 3068 3069 3070 3071 3072 3073 3074 3075 3076 3077 3078 3079 3080 3081 3082 3083 3084 3085 3086 3087 3088 3089 3090 3091 3092 3093 3094 3095 3096 3097 3098 3099 3100 3101 3102 3103 3104 3105 3106 3107 3108 3109 3110 3111 3112 3113 3114 3115 3116 3117 3118 3119 3120 3121 3122 3123 3124 3125 3126 3127 3128 3129 3130 3131 3132 3133 3134 3135 3136 3137 3138 3139 3140 3141 3142 3143 3144 3145 3146 3147 3148 3149 3150 3151 3152 3153 3154 3155 3156 3157 3158 3159 3160 3161 3162 3163 3164 3165 3166 3167 3168 3169 3170 3171 3172 3173 3174 3175 3176 3177 3178 3179 3180 3181 3182 3183 3184 3185 3186 3187 3188 3189 3190 3191 3192 3193 3194 3195 3196 3197 3198 3199 3200 3201 3202 3203 3204 3205 3206 3207 3208 3209 3210 3211 3212 3213 3214 3215 3216 3217 3218 3219 3220 3221 3222 3223 3224 3225 3226 3227 3228 3229 3230 3231 3232 3233 3234 3235 3236 3237 3238 3239 3240 3241 3242 3243 3244 3245 3246 3247 3248 3249 3250 3251 3252 3253 3254 3255 3256 3257 3258 3259 3260 3261 3262 3263 3264 3265 3266 3267 3268 3269 3270 3271 3272 3273 3274 3275 3276 3277 3278 3279 3280 3281 3282 3283 3284 3285 3286 3287 3288 3289 3290 3291 3292 3293 3294 3295 3296 3297 3298 3299 3300 3301 3302 3303 3304 3305 3306 3307 3308 3309 3310 3311 3312 3313 3314 3315 3316 3317 3318 3319 3320 3321 3322 3323 3324 3325 3326 3327 3328 3329 3330 3331 3332 3333 3334 3335 3336 3337 3338 3339 3340 3341 3342 3343 3344 3345 3346 3347 3348 3349 3350 3351 3352 3353 3354 3355 3356 3357 3358 3359 3360 3361 3362 3363 3364 3365 3366 3367 3368 3369 3370 3371 3372 3373 3374 3375 3376 3377 3378 3379 3380 3381 3382 3383 3384 3385 3386 3387 3388 3389 3390 3391 3392 3393 3394 3395 3396 3397 3398 3399 3400 3401 3402 3403 3404 3405 3406 3407 3408 3409 3410 3411 3412 3413 3414 3415 3416 3417 3418 3419 3420 3421 3422 3423 3424 3425 3426 3427 3428 3429 3430 3431 3432 3433 3434 3435 3436 3437 3438 3439 3440 3441 3442 3443 3444 3445 3446 3447 3448 3449 3450 3451 3452 3453 3454 3455 3456 3457 3458 3459 3460 3461 3462 3463 3464 3465 3466 3467 3468 3469 3470 3471 3472 3473 3474 3475 3476 3477 3478 3479 3480 3481 3482 3483 3484 3485 3486 3487 3488 3489 3490 3491 3492 3493 3494 3495 3496 3497 3498 3499 3500 3501 3502 3503 3504 3505 3506
/*
 * e-table.c - A graphical view of a Table.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) version 3.
 *
 * 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with the program; if not, see <http://www.gnu.org/licenses/>
 *
 *
 * Authors:
 *      Chris Lahey <clahey@ximian.com>
 *      Miguel de Icaza <miguel@ximian.com>
 *
 * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include "e-table.h"

#include <stdlib.h>
#include <string.h>
#include <stdio.h>

#include <gtk/gtk.h>
#include <glib/gi18n.h>
#include <glib/gstdio.h>
#include <gdk/gdkkeysyms.h>

#include <libgnomecanvas/libgnomecanvas.h>

#include "e-canvas-background.h"
#include "e-canvas-vbox.h"
#include "e-canvas.h"
#include "e-table-click-to-add.h"
#include "e-table-column-specification.h"
#include "e-table-group-leaf.h"
#include "e-table-header-item.h"
#include "e-table-header-utils.h"
#include "e-table-subset.h"
#include "e-table-utils.h"
#include "e-unicode.h"
#include "gal-a11y-e-table.h"

#define COLUMN_HEADER_HEIGHT 16

#define d(x)

#if d(!)0
#define e_table_item_leave_edit_(x) \
    (e_table_item_leave_edit ((x)), \
     g_print ("%s: e_table_item_leave_edit\n", __FUNCTION__))
#else
#define e_table_item_leave_edit_(x) (e_table_item_leave_edit((x)))
#endif

enum {
    CURSOR_CHANGE,
    CURSOR_ACTIVATED,
    SELECTION_CHANGE,
    DOUBLE_CLICK,
    RIGHT_CLICK,
    CLICK,
    KEY_PRESS,
    START_DRAG,
    STATE_CHANGE,
    WHITE_SPACE_EVENT,

    CUT_CLIPBOARD,
    COPY_CLIPBOARD,
    PASTE_CLIPBOARD,
    SELECT_ALL,

    TABLE_DRAG_BEGIN,
    TABLE_DRAG_END,
    TABLE_DRAG_DATA_GET,
    TABLE_DRAG_DATA_DELETE,

    TABLE_DRAG_LEAVE,
    TABLE_DRAG_MOTION,
    TABLE_DRAG_DROP,
    TABLE_DRAG_DATA_RECEIVED,

    LAST_SIGNAL
};

enum {
    PROP_0,
    PROP_LENGTH_THRESHOLD,
    PROP_MODEL,
    PROP_UNIFORM_ROW_HEIGHT,
    PROP_ALWAYS_SEARCH,
    PROP_USE_CLICK_TO_ADD,
    PROP_HADJUSTMENT,
    PROP_VADJUSTMENT,
    PROP_HSCROLL_POLICY,
    PROP_VSCROLL_POLICY,
    PROP_IS_EDITING
};

enum {
    ET_SCROLL_UP = 1 << 0,
    ET_SCROLL_DOWN = 1 << 1,
    ET_SCROLL_LEFT = 1 << 2,
    ET_SCROLL_RIGHT = 1 << 3
};

static guint et_signals[LAST_SIGNAL] = { 0 };

static void e_table_fill_table (ETable *e_table, ETableModel *model);
static gboolean changed_idle (gpointer data);

static void et_grab_focus (GtkWidget *widget);

static void et_drag_begin (GtkWidget *widget,
               GdkDragContext *context,
               ETable *et);
static void et_drag_end (GtkWidget *widget,
             GdkDragContext *context,
             ETable *et);
static void et_drag_data_get (GtkWidget *widget,
                 GdkDragContext *context,
                 GtkSelectionData *selection_data,
                 guint info,
                 guint time,
                 ETable *et);
static void et_drag_data_delete (GtkWidget *widget,
                GdkDragContext *context,
                ETable *et);

static void et_drag_leave (GtkWidget *widget,
              GdkDragContext *context,
              guint time,
              ETable *et);
static gboolean et_drag_motion (GtkWidget *widget,
                   GdkDragContext *context,
                   gint x,
                   gint y,
                   guint time,
                   ETable *et);
static gboolean et_drag_drop (GtkWidget *widget,
                 GdkDragContext *context,
                 gint x,
                 gint y,
                 guint time,
                 ETable *et);
static void et_drag_data_received (GtkWidget *widget,
                  GdkDragContext *context,
                  gint x,
                  gint y,
                  GtkSelectionData *selection_data,
                  guint info,
                  guint time,
                  ETable *et);

static gint et_focus (GtkWidget *container, GtkDirectionType direction);

static void scroll_off (ETable *et);
static void scroll_on (ETable *et, guint scroll_direction);

G_DEFINE_TYPE_WITH_CODE (ETable, e_table, GTK_TYPE_TABLE,
             G_IMPLEMENT_INTERFACE (GTK_TYPE_SCROLLABLE, NULL))

static void
et_disconnect_model (ETable *et)
{
    if (et->model == NULL)
        return;

    if (et->table_model_change_id != 0)
        g_signal_handler_disconnect (
            et->model, et->table_model_change_id);
    if (et->table_row_change_id != 0)
        g_signal_handler_disconnect (
            et->model, et->table_row_change_id);
    if (et->table_cell_change_id != 0)
        g_signal_handler_disconnect (
            et->model, et->table_cell_change_id);
    if (et->table_rows_inserted_id != 0)
        g_signal_handler_disconnect (
            et->model, et->table_rows_inserted_id);
    if (et->table_rows_deleted_id != 0)
        g_signal_handler_disconnect (
            et->model, et->table_rows_deleted_id);

    et->table_model_change_id = 0;
    et->table_row_change_id = 0;
    et->table_cell_change_id = 0;
    et->table_rows_inserted_id = 0;
    et->table_rows_deleted_id = 0;
}

static void
e_table_state_change (ETable *et)
{
    if (et->state_change_freeze)
        et->state_changed = TRUE;
    else
        g_signal_emit (et, et_signals[STATE_CHANGE], 0);
}

#define CHECK_HORIZONTAL(et) \
    if ((et)->horizontal_scrolling || (et)->horizontal_resize) \
        e_table_header_update_horizontal (et->header);

static void
clear_current_search_col (ETable *et)
{
    et->search_col_set = FALSE;
}

static ETableCol *
current_search_col (ETable *et)
{
    if (!et->search_col_set) {
        et->current_search_col =
            e_table_util_calculate_current_search_col (
                et->header,
                et->full_header,
                et->sort_info,
                et->always_search);
        et->search_col_set = TRUE;
    }

    return et->current_search_col;
}

static void
et_get_preferred_width (GtkWidget *widget,
                        gint *minimum,
                        gint *natural)
{
    ETable *et = E_TABLE (widget);

    GTK_WIDGET_CLASS (e_table_parent_class)->
        get_preferred_width (widget, minimum, natural);

    if (et->horizontal_resize) {
        *minimum = MAX (*minimum, et->header_width);
        *natural = MAX (*natural, et->header_width);
    }
}

static void
et_get_preferred_height (GtkWidget *widget,
                         gint *minimum,
                         gint *natural)
{
    GTK_WIDGET_CLASS (e_table_parent_class)->
        get_preferred_height (widget, minimum, natural);
}

static void
set_header_width (ETable *et)
{
    if (et->horizontal_resize) {
        et->header_width = e_table_header_min_width (et->header);
        gtk_widget_queue_resize (GTK_WIDGET (et));
    }
}

static void
structure_changed (ETableHeader *header,
                   ETable *et)
{
    e_table_state_change (et);
    set_header_width (et);
    clear_current_search_col (et);
}

static void
expansion_changed (ETableHeader *header,
                   ETable *et)
{
    e_table_state_change (et);
    set_header_width (et);
}

static void
dimension_changed (ETableHeader *header,
                   gint total_width,
                   ETable *et)
{
    set_header_width (et);
}

static void
disconnect_header (ETable *e_table)
{
    if (e_table->header == NULL)
        return;

    if (e_table->structure_change_id)
        g_signal_handler_disconnect (
            e_table->header, e_table->structure_change_id);
    if (e_table->expansion_change_id)
        g_signal_handler_disconnect (
            e_table->header, e_table->expansion_change_id);
    if (e_table->dimension_change_id)
        g_signal_handler_disconnect (
            e_table->header, e_table->dimension_change_id);

    g_object_unref (e_table->header);
    e_table->header = NULL;
}

static void
connect_header (ETable *e_table,
                ETableState *state)
{
    if (e_table->header != NULL)
        disconnect_header (e_table);

    e_table->header = e_table_state_to_header (
        GTK_WIDGET (e_table), e_table->full_header, state);

    e_table->structure_change_id = g_signal_connect (
        e_table->header, "structure_change",
        G_CALLBACK (structure_changed), e_table);
    e_table->expansion_change_id = g_signal_connect (
        e_table->header, "expansion_change",
        G_CALLBACK (expansion_changed), e_table);
    e_table->dimension_change_id = g_signal_connect (
        e_table->header, "dimension_change",
        G_CALLBACK (dimension_changed), e_table);
}

static void
et_dispose (GObject *object)
{
    ETable *et = E_TABLE (object);

    et_disconnect_model (et);

    if (et->search) {
        if (et->search_search_id)
            g_signal_handler_disconnect (
                et->search, et->search_search_id);
        if (et->search_accept_id)
            g_signal_handler_disconnect (
                et->search, et->search_accept_id);
        g_object_unref (et->search);
        et->search = NULL;
    }

    if (et->group_info_change_id) {
        g_signal_handler_disconnect (
            et->sort_info, et->group_info_change_id);
        et->group_info_change_id = 0;
    }

    if (et->sort_info_change_id) {
        g_signal_handler_disconnect (
            et->sort_info, et->sort_info_change_id);
        et->sort_info_change_id = 0;
    }

    if (et->reflow_idle_id) {
        g_source_remove (et->reflow_idle_id);
        et->reflow_idle_id = 0;
    }

    scroll_off (et);

    disconnect_header (et);

    if (et->model) {
        g_object_unref (et->model);
        et->model = NULL;
    }

    if (et->full_header) {
        g_object_unref (et->full_header);
        et->full_header = NULL;
    }

    if (et->sort_info) {
        g_object_unref (et->sort_info);
        et->sort_info = NULL;
    }

    if (et->sorter) {
        g_object_unref (et->sorter);
        et->sorter = NULL;
    }

    if (et->selection) {
        g_object_unref (et->selection);
        et->selection = NULL;
    }

    if (et->spec) {
        g_object_unref (et->spec);
        et->spec = NULL;
    }

    if (et->header_canvas != NULL) {
        gtk_widget_destroy (GTK_WIDGET (et->header_canvas));
        et->header_canvas = NULL;
    }

    if (et->site != NULL) {
        e_table_drag_source_unset (et);
        et->site = NULL;
    }

    if (et->table_canvas != NULL) {
        gtk_widget_destroy (GTK_WIDGET (et->table_canvas));
        et->table_canvas = NULL;
    }

    if (et->rebuild_idle_id != 0) {
        g_source_remove (et->rebuild_idle_id);
        et->rebuild_idle_id = 0;
    }

    g_free (et->click_to_add_message);
    et->click_to_add_message = NULL;

    g_free (et->domain);
    et->domain = NULL;

    G_OBJECT_CLASS (e_table_parent_class)->dispose (object);
}

static void
et_unrealize (GtkWidget *widget)
{
    scroll_off (E_TABLE (widget));

    if (GTK_WIDGET_CLASS (e_table_parent_class)->unrealize)
        GTK_WIDGET_CLASS (e_table_parent_class)->unrealize (widget);
}

static gboolean
check_row (ETable *et,
           gint model_row,
           gint col,
           ETableSearchFunc search,
           gchar *string)
{
    gconstpointer value;

    value = e_table_model_value_at (et->model, col, model_row);

    return search (value, string);
}

static gboolean
et_search_search (ETableSearch *search,
                  gchar *string,
                  ETableSearchFlags flags,
                  ETable *et)
{
    gint cursor;
    gint rows;
    gint i;
    ETableCol *col = current_search_col (et);

    if (col == NULL)
        return FALSE;

    rows = e_table_model_row_count (et->model);

    g_object_get (
        et->selection,
        "cursor_row", &cursor,
        NULL);

    if ((flags & E_TABLE_SEARCH_FLAGS_CHECK_CURSOR_FIRST) &&
        cursor < rows && cursor >= 0 &&
        check_row (et, cursor, col->spec->model_col, col->search, string))
        return TRUE;

    cursor = e_sorter_model_to_sorted (E_SORTER (et->sorter), cursor);

    for (i = cursor + 1; i < rows; i++) {
        gint model_row = e_sorter_sorted_to_model (E_SORTER (et->sorter), i);
        if (check_row (et, model_row, col->spec->model_col, col->search, string)) {
            e_selection_model_select_as_key_press (
                E_SELECTION_MODEL (et->selection),
                model_row, col->spec->model_col,
                GDK_CONTROL_MASK);
            return TRUE;
        }
    }

    for (i = 0; i < cursor; i++) {
        gint model_row = e_sorter_sorted_to_model (E_SORTER (et->sorter), i);
        if (check_row (et, model_row, col->spec->model_col, col->search, string)) {
            e_selection_model_select_as_key_press (
                E_SELECTION_MODEL (et->selection),
                model_row, col->spec->model_col,
                GDK_CONTROL_MASK);
            return TRUE;
        }
    }

    cursor = e_sorter_sorted_to_model (E_SORTER (et->sorter), cursor);

    /* Check if the cursor row is the only matching row. */
    return (!(flags & E_TABLE_SEARCH_FLAGS_CHECK_CURSOR_FIRST) &&
        cursor < rows && cursor >= 0 &&
        check_row (et, cursor, col->spec->model_col, col->search, string));
}

static void
et_search_accept (ETableSearch *search,
                  ETable *et)
{
    gint cursor;
    ETableCol *col = current_search_col (et);

    if (col == NULL)
        return;

    g_object_get (et->selection, "cursor_row", &cursor, NULL);

    e_selection_model_select_as_key_press (
        E_SELECTION_MODEL (et->selection),
        cursor, col->spec->model_col, 0);
}

static void
init_search (ETable *e_table)
{
    if (e_table->search != NULL)
        return;

    e_table->search           = e_table_search_new ();

    e_table->search_search_id = g_signal_connect (
        e_table->search, "search",
        G_CALLBACK (et_search_search), e_table);
    e_table->search_accept_id = g_signal_connect (
        e_table->search, "accept",
        G_CALLBACK (et_search_accept), e_table);
}

static void
et_finalize (GObject *object)
{
    ETable *et = E_TABLE (object);

    g_free (et->click_to_add_message);
    et->click_to_add_message = NULL;

    g_free (et->domain);
    et->domain = NULL;

    G_OBJECT_CLASS (e_table_parent_class)->finalize (object);
}

static void
e_table_init (ETable *e_table)
{
    gtk_widget_set_can_focus (GTK_WIDGET (e_table), TRUE);

    gtk_table_set_homogeneous (GTK_TABLE (e_table), FALSE);

    e_table->sort_info              = NULL;
    e_table->group_info_change_id   = 0;
    e_table->sort_info_change_id    = 0;
    e_table->structure_change_id    = 0;
    e_table->expansion_change_id    = 0;
    e_table->dimension_change_id    = 0;
    e_table->reflow_idle_id         = 0;
    e_table->scroll_idle_id         = 0;

    e_table->alternating_row_colors = 1;
    e_table->horizontal_draw_grid   = 1;
    e_table->vertical_draw_grid     = 1;
    e_table->draw_focus             = 1;
    e_table->cursor_mode            = E_CURSOR_SIMPLE;
    e_table->length_threshold       = 200;
    e_table->uniform_row_height     = FALSE;

    e_table->need_rebuild           = 0;
    e_table->rebuild_idle_id        = 0;

    e_table->horizontal_scrolling   = FALSE;
    e_table->horizontal_resize      = FALSE;

    e_table->click_to_add_message   = NULL;
    e_table->domain                 = NULL;

    e_table->drop_row               = -1;
    e_table->drop_col               = -1;
    e_table->site                   = NULL;

    e_table->do_drag                = 0;

    e_table->sorter                 = NULL;
    e_table->selection              = e_table_selection_model_new ();
    e_table->cursor_loc             = E_TABLE_CURSOR_LOC_NONE;
    e_table->spec                   = NULL;

    e_table->always_search          = g_getenv ("GAL_ALWAYS_SEARCH") ? TRUE : FALSE;

    e_table->search                 = NULL;
    e_table->search_search_id       = 0;
    e_table->search_accept_id       = 0;

    e_table->current_search_col     = NULL;

    e_table->header_width           = 0;

    e_table->state_changed      = FALSE;
    e_table->state_change_freeze    = 0;
}

/* Grab_focus handler for the ETable */
static void
et_grab_focus (GtkWidget *widget)
{
    ETable *e_table;

    e_table = E_TABLE (widget);

    gtk_widget_grab_focus (GTK_WIDGET (e_table->table_canvas));
}

/* Focus handler for the ETable */
static gint
et_focus (GtkWidget *container,
          GtkDirectionType direction)
{
    ETable *e_table;

    e_table = E_TABLE (container);

    if (gtk_container_get_focus_child (GTK_CONTAINER (container))) {
        gtk_container_set_focus_child (GTK_CONTAINER (container), NULL);
        return FALSE;
    }

    return gtk_widget_child_focus (GTK_WIDGET (e_table->table_canvas), direction);
}

static void
set_header_canvas_width (ETable *e_table)
{
    gdouble oldwidth, oldheight, width;

    if (!(e_table->header_item && e_table->header_canvas && e_table->table_canvas))
        return;

    gnome_canvas_get_scroll_region (
        GNOME_CANVAS (e_table->table_canvas),
        NULL, NULL, &width, NULL);
    gnome_canvas_get_scroll_region (
        GNOME_CANVAS (e_table->header_canvas),
        NULL, NULL, &oldwidth, &oldheight);

    if (oldwidth != width ||
        oldheight != E_TABLE_HEADER_ITEM (e_table->header_item)->height - 1)
        gnome_canvas_set_scroll_region (
            GNOME_CANVAS (e_table->header_canvas),
            0, 0, width, /*  COLUMN_HEADER_HEIGHT - 1 */
            E_TABLE_HEADER_ITEM (e_table->header_item)->height - 1);

}

static void
header_canvas_size_allocate (GtkWidget *widget,
                             GtkAllocation *alloc,
                             ETable *e_table)
{
    GtkAllocation allocation;

    set_header_canvas_width (e_table);

    gtk_widget_get_allocation (
        GTK_WIDGET (e_table->header_canvas), &allocation);

    /* When the header item is created ->height == 0,
     * as the font is only created when everything is realized.
     * So we set the usize here as well, so that the size of the
     * header is correct */
    if (allocation.height != E_TABLE_HEADER_ITEM (e_table->header_item)->height)
        g_object_set (
            e_table->header_canvas, "height-request",
            E_TABLE_HEADER_ITEM (e_table->header_item)->height,
            NULL);
}

static void
group_info_changed (ETableSortInfo *info,
                    ETable *et)
{
    gboolean will_be_grouped = e_table_sort_info_grouping_get_count (info) > 0;
    clear_current_search_col (et);
    if (et->is_grouped || will_be_grouped) {
        et->need_rebuild = TRUE;
        if (!et->rebuild_idle_id) {
            g_object_run_dispose (G_OBJECT (et->group));
            et->group = NULL;
            et->rebuild_idle_id = g_idle_add_full (20, changed_idle, et, NULL);
        }
    }
    e_table_state_change (et);
}

static void
sort_info_changed (ETableSortInfo *info,
                   ETable *et)
{
    clear_current_search_col (et);
    e_table_state_change (et);
}

static void
e_table_setup_header (ETable *e_table)
{
    gchar *pointer;
    e_table->header_canvas = GNOME_CANVAS (e_canvas_new ());

    gtk_widget_show (GTK_WIDGET (e_table->header_canvas));

    pointer = g_strdup_printf ("%p", (gpointer) e_table);

    e_table->header_item = gnome_canvas_item_new (
        gnome_canvas_root (e_table->header_canvas),
        e_table_header_item_get_type (),
        "ETableHeader", e_table->header,
        "full_header", e_table->full_header,
        "sort_info", e_table->sort_info,
        "dnd_code", pointer,
        "table", e_table,
        NULL);

    g_free (pointer);

    g_signal_connect (
        e_table->header_canvas, "size_allocate",
        G_CALLBACK (header_canvas_size_allocate), e_table);

    g_object_set (
        e_table->header_canvas, "height-request",
        E_TABLE_HEADER_ITEM (e_table->header_item)->height, NULL);
}

static gboolean
table_canvas_reflow_idle (ETable *e_table)
{
    gdouble height, width;
    gdouble oldheight, oldwidth;
    GtkAllocation allocation;

    gtk_widget_get_allocation (
        GTK_WIDGET (e_table->table_canvas), &allocation);

    g_object_get (
        e_table->canvas_vbox,
        "height", &height, "width", &width, NULL);
    height = MAX ((gint) height, allocation.height);
    width = MAX ((gint) width, allocation.width);
    /* I have no idea why this needs to be -1, but it works. */
    gnome_canvas_get_scroll_region (
        GNOME_CANVAS (e_table->table_canvas),
        NULL, NULL, &oldwidth, &oldheight);

    if (oldwidth != width - 1 ||
        oldheight != height - 1) {
        gnome_canvas_set_scroll_region (
            GNOME_CANVAS (e_table->table_canvas),
            0, 0, width - 1, height - 1);
        set_header_canvas_width (e_table);
    }
    e_table->reflow_idle_id = 0;
    return FALSE;
}

static void
table_canvas_size_allocate (GtkWidget *widget,
                            GtkAllocation *alloc,
                            ETable *e_table)
{
    gdouble width;
    gdouble height;
    GValue *val = g_new0 (GValue, 1);
    g_value_init (val, G_TYPE_DOUBLE);

    width = alloc->width;
    g_value_set_double (val, width);
    g_object_get (
        e_table->canvas_vbox,
        "height", &height,
        NULL);
    height = MAX ((gint) height, alloc->height);

    g_object_set (
        e_table->canvas_vbox,
        "width", width,
        NULL);
    g_object_set_property (G_OBJECT (e_table->header), "width", val);
    g_free (val);
    if (e_table->reflow_idle_id)
        g_source_remove (e_table->reflow_idle_id);
    table_canvas_reflow_idle (e_table);

    e_table->size_allocated = TRUE;

    if (e_table->need_rebuild && !e_table->rebuild_idle_id)
        e_table->rebuild_idle_id = g_idle_add_full (20, changed_idle, e_table, NULL);
}

static void
table_canvas_reflow (GnomeCanvas *canvas,
                     ETable *e_table)
{
    if (!e_table->reflow_idle_id)
        e_table->reflow_idle_id = g_idle_add_full (
            400, (GSourceFunc) table_canvas_reflow_idle,
            e_table, NULL);
}

static void
click_to_add_cursor_change (ETableClickToAdd *etcta,
                            gint row,
                            gint col,
                            ETable *et)
{
    if (et->cursor_loc == E_TABLE_CURSOR_LOC_TABLE) {
        e_selection_model_clear (E_SELECTION_MODEL (et->selection));
    }
    et->cursor_loc = E_TABLE_CURSOR_LOC_ETCTA;
}

static void
group_cursor_change (ETableGroup *etg,
                     gint row,
                     ETable *et)
{
    ETableCursorLoc old_cursor_loc;

    old_cursor_loc = et->cursor_loc;

    et->cursor_loc = E_TABLE_CURSOR_LOC_TABLE;
    g_signal_emit (et, et_signals[CURSOR_CHANGE], 0, row);

    if (old_cursor_loc == E_TABLE_CURSOR_LOC_ETCTA && et->click_to_add)
        e_table_click_to_add_commit (E_TABLE_CLICK_TO_ADD (et->click_to_add));
}

static void
group_cursor_activated (ETableGroup *etg,
                        gint row,
                        ETable *et)
{
    g_signal_emit (et, et_signals[CURSOR_ACTIVATED], 0, row);
}

static void
group_double_click (ETableGroup *etg,
                    gint row,
                    gint col,
                    GdkEvent *event,
                    ETable *et)
{
    g_signal_emit (et, et_signals[DOUBLE_CLICK], 0, row, col, event);
}

static gboolean
group_right_click (ETableGroup *etg,
                   gint row,
                   gint col,
                   GdkEvent *event,
                   ETable *et)
{
    gboolean return_val = FALSE;

    g_signal_emit (
        et, et_signals[RIGHT_CLICK], 0,
        row, col, event, &return_val);

    return return_val;
}

static gboolean
group_click (ETableGroup *etg,
             gint row,
             gint col,
             GdkEvent *event,
             ETable *et)
{
    gboolean return_val = 0;

    g_signal_emit (
        et, et_signals[CLICK], 0,
        row, col, event, &return_val);

    return return_val;
}

static gboolean
group_key_press (ETableGroup *etg,
                 gint row,
                 gint col,
                 GdkEvent *event,
                 ETable *et)
{
    gboolean return_val = FALSE;
    GdkEventKey *key = (GdkEventKey *) event;
    gint y, row_local, col_local;
    GtkAdjustment *adjustment;
    GtkScrollable *scrollable;
    gdouble page_size;
    gdouble upper;
    gdouble value;

    scrollable = GTK_SCROLLABLE (et->table_canvas);
    adjustment = gtk_scrollable_get_vadjustment (scrollable);

    switch (key->keyval) {
    case GDK_KEY_Page_Down:
    case GDK_KEY_KP_Page_Down:
        page_size = gtk_adjustment_get_page_size (adjustment);
        upper = gtk_adjustment_get_value (adjustment);
        value = gtk_adjustment_get_value (adjustment);

        y = CLAMP (value + (2 * page_size - 50), 0, upper);
        y -= value;
        e_table_get_cell_at (et, 30, y, &row_local, &col_local);

        if (row_local == -1)
            row_local = e_table_model_row_count (et->model) - 1;

        row_local = e_table_view_to_model_row (et, row_local);
        col_local = e_selection_model_cursor_col (E_SELECTION_MODEL (et->selection));
        e_selection_model_select_as_key_press (
            E_SELECTION_MODEL (et->selection),
            row_local, col_local, key->state);
        return_val = 1;
        break;
    case GDK_KEY_Page_Up:
    case GDK_KEY_KP_Page_Up:
        page_size = gtk_adjustment_get_page_size (adjustment);
        upper = gtk_adjustment_get_upper (adjustment);
        value = gtk_adjustment_get_value (adjustment);

        y = CLAMP (value - (page_size - 50), 0, upper);
        y -= value;
        e_table_get_cell_at (et, 30, y, &row_local, &col_local);

        if (row_local == -1)
            row_local = 0;

        row_local = e_table_view_to_model_row (et, row_local);
        col_local = e_selection_model_cursor_col (E_SELECTION_MODEL (et->selection));
        e_selection_model_select_as_key_press (
            E_SELECTION_MODEL (et->selection),
            row_local, col_local, key->state);
        return_val = 1;
        break;
    case GDK_KEY_BackSpace:
        init_search (et);
        if (e_table_search_backspace (et->search))
            return TRUE;
        /* Fall through */
    default:
        init_search (et);
        if ((key->state & ~(GDK_SHIFT_MASK | GDK_LOCK_MASK |
            GDK_MOD1_MASK | GDK_MOD2_MASK | GDK_MOD3_MASK |
            GDK_MOD4_MASK | GDK_MOD5_MASK)) == 0
            && ((key->keyval >= GDK_KEY_a && key->keyval <= GDK_KEY_z) ||
            (key->keyval >= GDK_KEY_A && key->keyval <= GDK_KEY_Z) ||
            (key->keyval >= GDK_KEY_0 && key->keyval <= GDK_KEY_9)))
            e_table_search_input_character (et->search, key->keyval);
        g_signal_emit (
            et, et_signals[KEY_PRESS], 0,
            row, col, event, &return_val);
        break;
    }
    return return_val;
}

static gboolean
group_start_drag (ETableGroup *etg,
                  gint row,
                  gint col,
                  GdkEvent *event,
                  ETable *et)
{
    gboolean return_val = TRUE;

    g_signal_emit (
        et, et_signals[START_DRAG], 0,
        row, col, event, &return_val);

    return return_val;
}

static void
et_table_model_changed (ETableModel *model,
                        ETable *et)
{
    et->need_rebuild = TRUE;
    if (!et->rebuild_idle_id) {
        g_object_run_dispose (G_OBJECT (et->group));
        et->group = NULL;
        et->rebuild_idle_id = g_idle_add_full (20, changed_idle, et, NULL);
    }
}

static void
et_table_row_changed (ETableModel *table_model,
                      gint row,
                      ETable *et)
{
    if (!et->need_rebuild) {
        if (e_table_group_remove (et->group, row))
            e_table_group_add (et->group, row);
        CHECK_HORIZONTAL (et);
    }
}

static void
et_table_cell_changed (ETableModel *table_model,
                       gint view_col,
                       gint row,
                       ETable *et)
{
    et_table_row_changed (table_model, row, et);
}

static void
et_table_rows_inserted (ETableModel *table_model,
                        gint row,
                        gint count,
                        ETable *et)
{
    /* This number has already been decremented. */
    gint row_count = e_table_model_row_count (table_model);
    if (!et->need_rebuild) {
        gint i;
        if (row != row_count - count)
            e_table_group_increment (et->group, row, count);
        for (i = 0; i < count; i++)
            e_table_group_add (et->group, row + i);
        CHECK_HORIZONTAL (et);
    }
}

static void
et_table_rows_deleted (ETableModel *table_model,
                       gint row,
                       gint count,
                       ETable *et)
{
    gint row_count = e_table_model_row_count (table_model);
    if (!et->need_rebuild) {
        gint i;
        for (i = 0; i < count; i++)
            e_table_group_remove (et->group, row + i);
        if (row != row_count)
            e_table_group_decrement (et->group, row, count);
        CHECK_HORIZONTAL (et);
    }
}

static void
group_is_editing_changed_cb (ETableClickToAdd *etcta,
                             GParamSpec *param,
                             ETable *table)
{
    g_return_if_fail (E_IS_TABLE (table));

    g_object_notify (G_OBJECT (table), "is-editing");
}

static void
et_build_groups (ETable *et)
{
    gboolean was_grouped = et->is_grouped;

    et->is_grouped = e_table_sort_info_grouping_get_count (et->sort_info) > 0;

    et->group = e_table_group_new (
        GNOME_CANVAS_GROUP (et->canvas_vbox),
        et->full_header,
        et->header,
        et->model,
        et->sort_info,
        0);

    if (et->use_click_to_add_end)
        e_canvas_vbox_add_item_start (
            E_CANVAS_VBOX (et->canvas_vbox),
            GNOME_CANVAS_ITEM (et->group));
    else
        e_canvas_vbox_add_item (
            E_CANVAS_VBOX (et->canvas_vbox),
            GNOME_CANVAS_ITEM (et->group));

    gnome_canvas_item_set (
        GNOME_CANVAS_ITEM (et->group),
        "alternating_row_colors", et->alternating_row_colors,
        "horizontal_draw_grid", et->horizontal_draw_grid,
        "vertical_draw_grid", et->vertical_draw_grid,
        "drawfocus", et->draw_focus,
        "cursor_mode", et->cursor_mode,
        "length_threshold", et->length_threshold,
        "uniform_row_height", et->uniform_row_height,
        "selection_model", et->selection,
        NULL);

    g_signal_connect (
        et->group, "cursor_change",
        G_CALLBACK (group_cursor_change), et);
    g_signal_connect (
        et->group, "cursor_activated",
        G_CALLBACK (group_cursor_activated), et);
    g_signal_connect (
        et->group, "double_click",
        G_CALLBACK (group_double_click), et);
    g_signal_connect (
        et->group, "right_click",
        G_CALLBACK (group_right_click), et);
    g_signal_connect (
        et->group, "click",
        G_CALLBACK (group_click), et);
    g_signal_connect (
        et->group, "key_press",
        G_CALLBACK (group_key_press), et);
    g_signal_connect (
        et->group, "start_drag",
        G_CALLBACK (group_start_drag), et);
    g_signal_connect (
        et->group, "notify::is-editing",
        G_CALLBACK (group_is_editing_changed_cb), et);

    if (!(et->is_grouped) && was_grouped)
        et_disconnect_model (et);

    if (et->is_grouped && (!was_grouped)) {
        et->table_model_change_id = g_signal_connect (
            et->model, "model_changed",
            G_CALLBACK (et_table_model_changed), et);

        et->table_row_change_id = g_signal_connect (
            et->model, "model_row_changed",
            G_CALLBACK (et_table_row_changed), et);

        et->table_cell_change_id = g_signal_connect (
            et->model, "model_cell_changed",
            G_CALLBACK (et_table_cell_changed), et);

        et->table_rows_inserted_id = g_signal_connect (
            et->model, "model_rows_inserted",
            G_CALLBACK (et_table_rows_inserted), et);

        et->table_rows_deleted_id = g_signal_connect (
            et->model, "model_rows_deleted",
            G_CALLBACK (et_table_rows_deleted), et);

    }

    if (et->is_grouped)
        e_table_fill_table (et, et->model);
}

static gboolean
changed_idle (gpointer data)
{
    ETable *et = E_TABLE (data);

    /* Wait until we have a valid size allocation. */
    if (et->need_rebuild && et->size_allocated) {
        GtkWidget *widget;
        GtkAllocation allocation;

        if (et->group)
            g_object_run_dispose (G_OBJECT (et->group));
        et_build_groups (et);

        widget = GTK_WIDGET (et->table_canvas);
        gtk_widget_get_allocation (widget, &allocation);

        g_object_set (
            et->canvas_vbox,
            "width", (gdouble) allocation.width,
            NULL);

        table_canvas_size_allocate (widget, &allocation, et);

        et->need_rebuild = 0;
    }

    et->rebuild_idle_id = 0;

    CHECK_HORIZONTAL (et);

    return FALSE;
}

static void
et_canvas_realize (GtkWidget *canvas,
                   ETable *e_table)
{
    GtkWidget *widget;
    GtkStyle *style;

    widget = GTK_WIDGET (e_table->table_canvas);
    style = gtk_widget_get_style (widget);

    gnome_canvas_item_set (
        e_table->white_item,
        "fill_color_gdk", &style->base[GTK_STATE_NORMAL],
        NULL);

    CHECK_HORIZONTAL (e_table);
    set_header_width (e_table);
}

static gboolean
white_item_event (GnomeCanvasItem *white_item,
                  GdkEvent *event,
                  ETable *e_table)
{
    gboolean return_val = 0;

    g_signal_emit (
        e_table, et_signals[WHITE_SPACE_EVENT], 0,
        event, &return_val);

    return return_val;
}

static void
et_eti_leave_edit (ETable *et)
{
    GnomeCanvas *canvas = et->table_canvas;

    if (gtk_widget_has_focus (GTK_WIDGET (canvas))) {
        GnomeCanvasItem *item = GNOME_CANVAS (canvas)->focused_item;

        if (E_IS_TABLE_ITEM (item)) {
            e_table_item_leave_edit_(E_TABLE_ITEM (item));
        }
    }
}

static gint
et_canvas_root_event (GnomeCanvasItem *root,
                      GdkEvent *event,
                      ETable *e_table)
{
    switch (event->type) {
    case GDK_BUTTON_PRESS:
    case GDK_2BUTTON_PRESS:
    case GDK_BUTTON_RELEASE:
        if (event->button.button != 4 && event->button.button != 5) {
            et_eti_leave_edit (e_table);
            return TRUE;
        }
        break;
    default:
        break;
    }

    return FALSE;
}

/* Finds the first descendant of the group that is an ETableItem and focuses it */
static void
focus_first_etable_item (ETableGroup *group)
{
    GnomeCanvasGroup *cgroup;
    GList *l;

    cgroup = GNOME_CANVAS_GROUP (group);

    for (l = cgroup->item_list; l; l = l->next) {
        GnomeCanvasItem *i;

        i = GNOME_CANVAS_ITEM (l->data);

        if (E_IS_TABLE_GROUP (i))
            focus_first_etable_item (E_TABLE_GROUP (i));
        else if (E_IS_TABLE_ITEM (i)) {
            e_table_item_set_cursor (E_TABLE_ITEM (i), 0, 0);
            gnome_canvas_item_grab_focus (i);
        }
    }
}

/* Handler for focus events in the table_canvas; we have to repaint ourselves
 * always, and also give the focus to some ETableItem if we get focused.
 */
static gint
table_canvas_focus_event_cb (GtkWidget *widget,
                             GdkEventFocus *event,
                             gpointer data)
{
    GnomeCanvas *canvas;
    ECanvas *ecanvas;
    ETable *etable;

    gtk_widget_queue_draw (widget);
    canvas = GNOME_CANVAS (widget);
    ecanvas = E_CANVAS (widget);

    if (!event->in) {
        gtk_im_context_focus_out (ecanvas->im_context);
        return FALSE;
    } else {
        gtk_im_context_focus_in (ecanvas->im_context);
    }

    etable = E_TABLE (data);

    if (e_table_model_row_count (etable->model) < 1
        && (etable->click_to_add)
        && !(E_TABLE_CLICK_TO_ADD (etable->click_to_add)->row)) {
        gnome_canvas_item_grab_focus (etable->canvas_vbox);
        gnome_canvas_item_grab_focus (etable->click_to_add);
    } else if (!canvas->focused_item && etable->group) {
        focus_first_etable_item (etable->group);
    } else if (canvas->focused_item) {
        ESelectionModel *selection = (ESelectionModel *) etable->selection;

        /* check whether click_to_add already got the focus */
        if (etable->click_to_add) {
            GnomeCanvasItem *row = E_TABLE_CLICK_TO_ADD (etable->click_to_add)->row;
            if (canvas->focused_item == row)
                return TRUE;
        }

        if (e_selection_model_cursor_row (selection) == -1)
            focus_first_etable_item (etable->group);
    }

    return FALSE;
}

static gboolean
canvas_vbox_event (ECanvasVbox *vbox,
                   GdkEventKey *key,
                   ETable *etable)
{
    if (key->type != GDK_KEY_PRESS  &&
        key->type != GDK_KEY_RELEASE) {
        return FALSE;
    }
    switch (key->keyval) {
        case GDK_KEY_Tab:
        case GDK_KEY_KP_Tab:
        case GDK_KEY_ISO_Left_Tab:
            if ((key->state & GDK_CONTROL_MASK) && etable->click_to_add) {
                gnome_canvas_item_grab_focus (etable->click_to_add);
                break;
            }
        default:
            return FALSE;
    }

    return TRUE;
}

static void
click_to_add_is_editing_changed_cb (ETableClickToAdd *etcta,
                                    GParamSpec *param,
                                    ETable *table)
{
    g_return_if_fail (E_IS_TABLE (table));

    g_object_notify (G_OBJECT (table), "is-editing");
}

static gboolean
click_to_add_event (ETableClickToAdd *etcta,
                    GdkEventKey *key,
                    ETable *etable)
{
    if (key->type != GDK_KEY_PRESS  &&
        key->type != GDK_KEY_RELEASE) {
        return FALSE;
    }
    switch (key->keyval) {
        case GDK_KEY_Tab:
        case GDK_KEY_KP_Tab:
        case GDK_KEY_ISO_Left_Tab:
            if (key->state & GDK_CONTROL_MASK) {
                if (etable->group) {
                    if (e_table_model_row_count (etable->model) > 0)
                        focus_first_etable_item (etable->group);
                    else
                        gtk_widget_child_focus (
                            gtk_widget_get_toplevel (
                            GTK_WIDGET (etable->table_canvas)),
                            GTK_DIR_TAB_FORWARD);
                    break;
                }
            }
        default:
            return FALSE;
    }

    return FALSE;
}

static void
e_table_setup_table (ETable *e_table,
                     ETableHeader *full_header,
                     ETableHeader *header,
                     ETableModel *model)
{
    GtkWidget *widget;
    GtkStyle *style;

    e_table->table_canvas = GNOME_CANVAS (e_canvas_new ());
    g_signal_connect (
        e_table->table_canvas, "size_allocate",
        G_CALLBACK (table_canvas_size_allocate), e_table);
    g_signal_connect (
        e_table->table_canvas, "focus_in_event",
        G_CALLBACK (table_canvas_focus_event_cb), e_table);
    g_signal_connect (
        e_table->table_canvas, "focus_out_event",
        G_CALLBACK (table_canvas_focus_event_cb), e_table);

    g_signal_connect (
        e_table, "drag_begin",
        G_CALLBACK (et_drag_begin), e_table);
    g_signal_connect (
        e_table, "drag_end",
        G_CALLBACK (et_drag_end), e_table);
    g_signal_connect (
        e_table, "drag_data_get",
        G_CALLBACK (et_drag_data_get), e_table);
    g_signal_connect (
        e_table, "drag_data_delete",
        G_CALLBACK (et_drag_data_delete), e_table);
    g_signal_connect (
        e_table, "drag_motion",
        G_CALLBACK (et_drag_motion), e_table);
    g_signal_connect (
        e_table, "drag_leave",
        G_CALLBACK (et_drag_leave), e_table);
    g_signal_connect (
        e_table, "drag_drop",
        G_CALLBACK (et_drag_drop), e_table);
    g_signal_connect (
        e_table, "drag_data_received",
        G_CALLBACK (et_drag_data_received), e_table);

    g_signal_connect (
        e_table->table_canvas, "reflow",
        G_CALLBACK (table_canvas_reflow), e_table);

    widget = GTK_WIDGET (e_table->table_canvas);
    style = gtk_widget_get_style (widget);

    gtk_widget_show (widget);

    e_table->white_item = gnome_canvas_item_new (
        gnome_canvas_root (e_table->table_canvas),
        e_canvas_background_get_type (),
        "fill_color_gdk", &style->base[GTK_STATE_NORMAL],
        NULL);

    g_signal_connect (
        e_table->white_item, "event",
        G_CALLBACK (white_item_event), e_table);

    g_signal_connect (
        e_table->table_canvas, "realize",
        G_CALLBACK (et_canvas_realize), e_table);

    g_signal_connect (
        gnome_canvas_root (e_table->table_canvas), "event",
        G_CALLBACK (et_canvas_root_event), e_table);

    e_table->canvas_vbox = gnome_canvas_item_new (
        gnome_canvas_root (e_table->table_canvas),
        e_canvas_vbox_get_type (),
        "spacing", 10.0,
        NULL);

    g_signal_connect (
        e_table->canvas_vbox, "event",
        G_CALLBACK (canvas_vbox_event), e_table);

    et_build_groups (e_table);

    if (e_table->use_click_to_add) {
        e_table->click_to_add = gnome_canvas_item_new (
            GNOME_CANVAS_GROUP (e_table->canvas_vbox),
            e_table_click_to_add_get_type (),
            "header", e_table->header,
            "model", e_table->model,
            "message", e_table->click_to_add_message,
            NULL);

        if (e_table->use_click_to_add_end)
            e_canvas_vbox_add_item (
                E_CANVAS_VBOX (e_table->canvas_vbox),
                e_table->click_to_add);
        else
            e_canvas_vbox_add_item_start (
                E_CANVAS_VBOX (e_table->canvas_vbox),
                e_table->click_to_add);

        g_signal_connect (
            e_table->click_to_add, "event",
            G_CALLBACK (click_to_add_event), e_table);
        g_signal_connect (
            e_table->click_to_add, "cursor_change",
            G_CALLBACK (click_to_add_cursor_change), e_table);
        g_signal_connect (
            e_table->click_to_add, "notify::is-editing",
            G_CALLBACK (click_to_add_is_editing_changed_cb), e_table);
    }
}

static void
e_table_fill_table (ETable *e_table,
                    ETableModel *model)
{
    e_table_group_add_all (e_table->group);
}

/**
 * e_table_set_state_object:
 * @e_table: The #ETable object to modify
 * @state: The #ETableState to use
 *
 * This routine sets the state of the #ETable from the given
 * #ETableState.
 *
 **/
void
e_table_set_state_object (ETable *e_table,
                          ETableState *state)
{
    GValue *val;
    GtkWidget *widget;
    GtkAllocation allocation;

    val = g_new0 (GValue, 1);
    g_value_init (val, G_TYPE_DOUBLE);

    connect_header (e_table, state);

    widget = GTK_WIDGET (e_table->table_canvas);
    gtk_widget_get_allocation (widget, &allocation);

    g_value_set_double (val, (gdouble) allocation.width);
    g_object_set_property (G_OBJECT (e_table->header), "width", val);
    g_free (val);

    if (e_table->sort_info) {
        if (e_table->group_info_change_id)
            g_signal_handler_disconnect (
                e_table->sort_info,
                e_table->group_info_change_id);
        if (e_table->sort_info_change_id)
            g_signal_handler_disconnect (
                e_table->sort_info,
                e_table->sort_info_change_id);
        g_object_unref (e_table->sort_info);
    }
    if (state->sort_info) {
        e_table->sort_info = e_table_sort_info_duplicate (state->sort_info);
        e_table_sort_info_set_can_group (
            e_table->sort_info, e_table->allow_grouping);
        e_table->group_info_change_id = g_signal_connect (
            e_table->sort_info, "group_info_changed",
            G_CALLBACK (group_info_changed), e_table);

        e_table->sort_info_change_id = g_signal_connect (
            e_table->sort_info, "sort_info_changed",
            G_CALLBACK (sort_info_changed), e_table);
    }
    else
        e_table->sort_info = NULL;

    if (e_table->sorter)
        g_object_set (
            e_table->sorter,
            "sort_info", e_table->sort_info,
            NULL);
    if (e_table->header_item)
        g_object_set (
            e_table->header_item,
            "ETableHeader", e_table->header,
            "sort_info", e_table->sort_info,
            NULL);
    if (e_table->click_to_add)
        g_object_set (
            e_table->click_to_add,
            "header", e_table->header,
            NULL);

    e_table->need_rebuild = TRUE;
    if (!e_table->rebuild_idle_id)
        e_table->rebuild_idle_id = g_idle_add_full (20, changed_idle, e_table, NULL);

    e_table_state_change (e_table);
}

/**
 * e_table_load_state:
 * @e_table: The #ETable object to modify
 * @filename: name of the file to use
 *
 * This routine sets the state of the #ETable from a file.
 *
 **/
void
e_table_load_state (ETable *e_table,
                    const gchar *filename)
{
    ETableState *state;

    g_return_if_fail (E_IS_TABLE (e_table));
    g_return_if_fail (filename != NULL);

    state = e_table_state_new (e_table->spec);
    e_table_state_load_from_file (state, filename);

    if (state->col_count > 0)
        e_table_set_state_object (e_table, state);

    g_object_unref (state);
}

/**
 * e_table_get_state_object:
 * @e_table: #ETable object to act on
 *
 * Builds an #ETableState corresponding to the current state of the
 * #ETable.
 *
 * Return value:
 * The %ETableState object generated.
 **/
ETableState *
e_table_get_state_object (ETable *e_table)
{
    ETableState *state;
    GPtrArray *columns;
    gint full_col_count;
    gint i, j;

    columns = e_table_specification_ref_columns (e_table->spec);

    state = e_table_state_new (e_table->spec);

    g_clear_object (&state->sort_info);
    state->sort_info = g_object_ref (e_table->sort_info);

    state->col_count = e_table_header_count (e_table->header);
    full_col_count = e_table_header_count (e_table->full_header);

    state->column_specs = g_new (
        ETableColumnSpecification *, state->col_count);
    state->expansions = g_new (gdouble, state->col_count);

    for (i = 0; i < state->col_count; i++) {
        ETableCol *col = e_table_header_get_column (e_table->header, i);
        state->column_specs[i] = NULL;
        for (j = 0; j < full_col_count; j++) {
            if (col->spec->model_col == e_table_header_index (e_table->full_header, j)) {
                state->column_specs[i] =
                    g_object_ref (columns->pdata[j]);
                break;
            }
        }
        state->expansions[i] = col->expansion;
    }

    g_ptr_array_unref (columns);

    return state;
}

/**
 * e_table_save_state:
 * @e_table: The #ETable to act on
 * @filename: name of the file to save to
 *
 * Saves the state of the @e_table object into the file pointed by
 * @filename.
 *
 **/
void
e_table_save_state (ETable *e_table,
                    const gchar *filename)
{
    ETableState *state;

    state = e_table_get_state_object (e_table);
    e_table_state_save_to_file (state, filename);
    g_object_unref (state);
}

static void
et_selection_model_selection_changed (ETableGroup *etg,
                                      ETable *et)
{
    g_signal_emit (et, et_signals[SELECTION_CHANGE], 0);
}

static void
et_selection_model_selection_row_changed (ETableGroup *etg,
                                          gint row,
                                          ETable *et)
{
    g_signal_emit (et, et_signals[SELECTION_CHANGE], 0);
}

static ETable *
et_real_construct (ETable *e_table,
                   ETableModel *etm,
                   ETableExtras *ete,
                   ETableSpecification *specification,
                   ETableState *state)
{
    gint row = 0;
    gint col_count, i;
    GValue *val;
    GtkAdjustment *adjustment;
    GtkScrollable *scrollable;

    val = g_new0 (GValue, 1);
    g_value_init (val, G_TYPE_OBJECT);

    if (ete)
        g_object_ref (ete);
    else {
        ete = e_table_extras_new ();
    }

    e_table->domain = g_strdup (specification->domain);

    e_table->use_click_to_add = specification->click_to_add;
    e_table->use_click_to_add_end = specification->click_to_add_end;
    e_table->click_to_add_message = specification->click_to_add_message ?
        g_strdup (
            dgettext (e_table->domain,
        specification->click_to_add_message)) : NULL;
    e_table->alternating_row_colors = specification->alternating_row_colors;
    e_table->horizontal_draw_grid = specification->horizontal_draw_grid;
    e_table->vertical_draw_grid = specification->vertical_draw_grid;
    e_table->draw_focus = specification->draw_focus;
    e_table->cursor_mode = specification->cursor_mode;
    e_table->full_header = e_table_spec_to_full_header (specification, ete);

    col_count = e_table_header_count (e_table->full_header);
    for (i = 0; i < col_count; i++) {
        ETableCol *col = e_table_header_get_column (e_table->full_header, i);
        if (col && col->search) {
            e_table->current_search_col = col;
            e_table->search_col_set = TRUE;
            break;
        }
    }

    e_table->model = etm;
    g_object_ref (etm);

    connect_header (e_table, state);
    e_table->horizontal_scrolling = specification->horizontal_scrolling;
    e_table->horizontal_resize = specification->horizontal_resize;
    e_table->allow_grouping = specification->allow_grouping;

    e_table->sort_info = g_object_ref (state->sort_info);

    e_table_sort_info_set_can_group (
        e_table->sort_info, e_table->allow_grouping);

    e_table->group_info_change_id = g_signal_connect (
        e_table->sort_info, "group_info_changed",
        G_CALLBACK (group_info_changed), e_table);

    e_table->sort_info_change_id = g_signal_connect (
        e_table->sort_info, "sort_info_changed",
        G_CALLBACK (sort_info_changed), e_table);

    g_value_set_object (val, e_table->sort_info);
    g_object_set_property (G_OBJECT (e_table->header), "sort_info", val);
    g_free (val);

    e_table->sorter = e_table_sorter_new (
        etm, e_table->full_header, e_table->sort_info);

    g_object_set (
        e_table->selection,
        "model", etm,
        "selection_mode", specification->selection_mode,
        "cursor_mode", specification->cursor_mode,
        "sorter", e_table->sorter,
        "header", e_table->header,
        NULL);

    g_signal_connect (
        e_table->selection, "selection_changed",
        G_CALLBACK (et_selection_model_selection_changed), e_table);
    g_signal_connect (
        e_table->selection, "selection_row_changed",
        G_CALLBACK (et_selection_model_selection_row_changed), e_table);

    if (!specification->no_headers)
        e_table_setup_header (e_table);

    e_table_setup_table (
        e_table, e_table->full_header, e_table->header, etm);
    e_table_fill_table (e_table, etm);

    scrollable = GTK_SCROLLABLE (e_table->table_canvas);

    adjustment = gtk_scrollable_get_vadjustment (scrollable);
    gtk_adjustment_set_step_increment (adjustment, 20);

    adjustment = gtk_scrollable_get_hadjustment (scrollable);
    gtk_adjustment_set_step_increment (adjustment, 20);

    if (!specification->no_headers) {
        /* The header */
        gtk_table_attach (
            GTK_TABLE (e_table), GTK_WIDGET (e_table->header_canvas),
            0, 1, 0 + row, 1 + row,
            GTK_FILL | GTK_EXPAND,
            GTK_FILL, 0, 0);
        row++;
    }
    gtk_table_attach (
        GTK_TABLE (e_table), GTK_WIDGET (e_table->table_canvas),
        0, 1, 0 + row, 1 + row,
        GTK_FILL | GTK_EXPAND,
        GTK_FILL | GTK_EXPAND,
        0, 0);

    g_object_unref (ete);

    return e_table;
}

/**
 * e_table_construct:
 * @e_table: The newly created #ETable object.
 * @etm: The model for this table.
 * @ete: An optional #ETableExtras.  (%NULL is valid.)
 * @specification: an #ETableSpecification
 *
 * This is the internal implementation of e_table_new() for use by
 * subclasses or language bindings.  See e_table_new() for details.
 *
 * Return value:
 * The passed in value @e_table or %NULL if there's an error.
 **/
ETable *
e_table_construct (ETable *e_table,
                   ETableModel *etm,
                   ETableExtras *ete,
                   ETableSpecification *specification)
{
    ETableState *state;

    g_return_val_if_fail (E_IS_TABLE (e_table), NULL);
    g_return_val_if_fail (E_IS_TABLE_MODEL (etm), NULL);
    g_return_val_if_fail (ete == NULL || E_IS_TABLE_EXTRAS (ete), NULL);
    g_return_val_if_fail (E_IS_TABLE_SPECIFICATION (specification), NULL);

    g_object_ref (etm);

    state = g_object_ref (specification->state);

    e_table = et_real_construct (e_table, etm, ete, specification, state);

    e_table->spec = g_object_ref (specification);
    g_object_unref (state);

    return e_table;
}

/**
 * e_table_new:
 * @etm: The model for this table.
 * @ete: An optional #ETableExtras.  (%NULL is valid.)
 * @specification: an #ETableSpecification
 *
 * This function creates an #ETable from the given parameters.  The
 * #ETableModel is a table model to be represented.  The #ETableExtras
 * is an optional set of pixbufs, cells, and sorting functions to be
 * used when interpreting the spec.  If you pass in %NULL it uses the
 * default #ETableExtras.  (See e_table_extras_new()).
 *
 * @specification is the specification of the set of viewable columns and the
 * default sorting state and such.  @state is an optional string specifying
 * the current sorting state and such.
 *
 * Return value:
 * The newly created #ETable or %NULL if there's an error.
 **/
GtkWidget *
e_table_new (ETableModel *etm,
             ETableExtras *ete,
             ETableSpecification *specification)
{
    ETable *e_table;

    g_return_val_if_fail (E_IS_TABLE_MODEL (etm), NULL);
    g_return_val_if_fail (ete == NULL || E_IS_TABLE_EXTRAS (ete), NULL);
    g_return_val_if_fail (E_IS_TABLE_SPECIFICATION (specification), NULL);

    e_table = g_object_new (E_TYPE_TABLE, NULL);

    e_table = e_table_construct (e_table, etm, ete, specification);

    return GTK_WIDGET (e_table);
}

/**
 * e_table_set_cursor_row:
 * @e_table: The #ETable to set the cursor row of
 * @row: The row number
 *
 * Sets the cursor row and the selection to the given row number.
 **/
void
e_table_set_cursor_row (ETable *e_table,
                        gint row)
{
    g_return_if_fail (E_IS_TABLE (e_table));
    g_return_if_fail (row >= 0);

    g_object_set (
        e_table->selection,
        "cursor_row", row,
        NULL);
}

/**
 * e_table_get_cursor_row:
 * @e_table: The #ETable to query
 *
 * Calculates the cursor row.  -1 means that we don't have a cursor.
 *
 * Return value:
 * Cursor row
 **/
gint
e_table_get_cursor_row (ETable *e_table)
{
    gint row;
    g_return_val_if_fail (E_IS_TABLE (e_table), -1);

    g_object_get (
        e_table->selection,
        "cursor_row", &row,
        NULL);
    return row;
}

/**
 * e_table_selected_row_foreach:
 * @e_table: The #ETable to act on
 * @callback: The callback function to call
 * @closure: The value passed to the callback's closure argument
 *
 * Calls the given @callback function once for every selected row.
 *
 * If you change the selection or delete or add rows to the table
 * during these callbacks, problems can occur.  A standard thing to do
 * is to create a list of rows or objects the function is called upon
 * and then act upon that list. (In inverse order if it's rows.)
 **/
void
e_table_selected_row_foreach (ETable *e_table,
                              EForeachFunc callback,
                              gpointer closure)
{
    g_return_if_fail (E_IS_TABLE (e_table));

    e_selection_model_foreach (E_SELECTION_MODEL (e_table->selection),
                             callback,
                             closure);
}

/**
 * e_table_selected_count:
 * @e_table: The #ETable to query
 *
 * Counts the number of selected rows.
 *
 * Return value:
 * The number of rows selected.
 **/
gint
e_table_selected_count (ETable *e_table)
{
    g_return_val_if_fail (E_IS_TABLE (e_table), -1);

    return e_selection_model_selected_count (E_SELECTION_MODEL (e_table->selection));
}

/**
 * e_table_select_all:
 * @table: The #ETable to modify
 *
 * Selects all the rows in @table.
 **/
void
e_table_select_all (ETable *table)
{
    g_return_if_fail (E_IS_TABLE (table));

    e_selection_model_select_all (E_SELECTION_MODEL (table->selection));
}

/**
 * e_table_get_printable:
 * @e_table: #ETable to query
 *
 * Used for printing your #ETable.
 *
 * Return value:
 * The #EPrintable to print.
 **/
EPrintable *
e_table_get_printable (ETable *e_table)
{
    g_return_val_if_fail (E_IS_TABLE (e_table), NULL);

    return e_table_group_get_printable (e_table->group);
}

/**
 * e_table_right_click_up:
 * @table: The #ETable to modify.
 *
 * Call this function when you're done handling the right click if you
 * return TRUE from the "right_click" signal.
 **/
void
e_table_right_click_up (ETable *table)
{
    e_selection_model_right_click_up (E_SELECTION_MODEL (table->selection));
}

/**
 * e_table_commit_click_to_add:
 * @table: The #ETable to modify
 *
 * Commits the current values in the click to add to the table.
 **/
void
e_table_commit_click_to_add (ETable *table)
{
    et_eti_leave_edit (table);
    if (table->click_to_add)
        e_table_click_to_add_commit (
            E_TABLE_CLICK_TO_ADD (table->click_to_add));
}

static void
et_get_property (GObject *object,
                 guint property_id,
                 GValue *value,
                 GParamSpec *pspec)
{
    ETable *etable = E_TABLE (object);

    switch (property_id) {
    case PROP_MODEL:
        g_value_set_object (value, etable->model);
        break;
    case PROP_UNIFORM_ROW_HEIGHT:
        g_value_set_boolean (value, etable->uniform_row_height);
        break;
    case PROP_ALWAYS_SEARCH:
        g_value_set_boolean (value, etable->always_search);
        break;
    case PROP_USE_CLICK_TO_ADD:
        g_value_set_boolean (value, etable->use_click_to_add);
        break;
    case PROP_HADJUSTMENT:
        if (etable->table_canvas)
            g_object_get_property (
                G_OBJECT (etable->table_canvas),
                "hadjustment", value);
        else
            g_value_set_object (value, NULL);
        break;
    case PROP_VADJUSTMENT:
        if (etable->table_canvas)
            g_object_get_property (
                G_OBJECT (etable->table_canvas),
                "vadjustment", value);
        else
            g_value_set_object (value, NULL);
        break;
    case PROP_HSCROLL_POLICY:
        if (etable->table_canvas)
            g_object_get_property (
                G_OBJECT (etable->table_canvas),
                "hscroll-policy", value);
        else
            g_value_set_enum (value, 0);
        break;
    case PROP_VSCROLL_POLICY:
        if (etable->table_canvas)
            g_object_get_property (
                G_OBJECT (etable->table_canvas),
                "vscroll-policy", value);
        else
            g_value_set_enum (value, 0);
        break;
    case PROP_IS_EDITING:
        g_value_set_boolean (value, e_table_is_editing (etable));
        break;
    default:
        break;
    }
}

typedef struct {
    gchar     *arg;
    gboolean  setting;
} bool_closure;

static void
et_set_property (GObject *object,
                 guint property_id,
                 const GValue *value,
                 GParamSpec *pspec)
{
    ETable *etable = E_TABLE (object);

    switch (property_id) {
    case PROP_LENGTH_THRESHOLD:
        etable->length_threshold = g_value_get_int (value);
        if (etable->group) {
            gnome_canvas_item_set (
                GNOME_CANVAS_ITEM (etable->group),
                "length_threshold",
                etable->length_threshold,
                NULL);
        }
        break;
    case PROP_UNIFORM_ROW_HEIGHT:
        etable->uniform_row_height = g_value_get_boolean (value);
        if (etable->group) {
            gnome_canvas_item_set (
                GNOME_CANVAS_ITEM (etable->group),
                "uniform_row_height",
                etable->uniform_row_height,
                NULL);
        }
        break;
    case PROP_ALWAYS_SEARCH:
        if (etable->always_search == g_value_get_boolean (value))
            return;

        etable->always_search = g_value_get_boolean (value);
        clear_current_search_col (etable);
        break;
    case PROP_USE_CLICK_TO_ADD:
        if (etable->use_click_to_add == g_value_get_boolean (value))
            return;

        etable->use_click_to_add = g_value_get_boolean (value);
        clear_current_search_col (etable);

        if (etable->use_click_to_add) {
            etable->click_to_add = gnome_canvas_item_new (
                GNOME_CANVAS_GROUP (etable->canvas_vbox),
                e_table_click_to_add_get_type (),
                "header", etable->header,
                "model", etable->model,
                "message", etable->click_to_add_message,
                NULL);

            if (etable->use_click_to_add_end)
                e_canvas_vbox_add_item (
                    E_CANVAS_VBOX (etable->canvas_vbox),
                    etable->click_to_add);
            else
                e_canvas_vbox_add_item_start (
                    E_CANVAS_VBOX (etable->canvas_vbox),
                    etable->click_to_add);

            g_signal_connect (
                etable->click_to_add, "event",
                G_CALLBACK (click_to_add_event), etable);
            g_signal_connect (
                etable->click_to_add, "cursor_change",
                G_CALLBACK (click_to_add_cursor_change),
                etable);
            g_signal_connect (
                etable->click_to_add, "notify::is-editing",
                G_CALLBACK (click_to_add_is_editing_changed_cb), etable);
        } else {
            g_object_run_dispose (G_OBJECT (etable->click_to_add));
            etable->click_to_add = NULL;
        }
        break;
    case PROP_HADJUSTMENT:
        if (etable->table_canvas)
            g_object_set_property (
                G_OBJECT (etable->table_canvas),
                "hadjustment", value);
        break;
    case PROP_VADJUSTMENT:
        if (etable->table_canvas)
            g_object_set_property (
                G_OBJECT (etable->table_canvas),
                "vadjustment", value);
        break;
    case PROP_HSCROLL_POLICY:
        if (etable->table_canvas)
            g_object_set_property (
                G_OBJECT (etable->table_canvas),
                "hscroll-policy", value);
        break;
    case PROP_VSCROLL_POLICY:
        if (etable->table_canvas)
            g_object_set_property (
                G_OBJECT (etable->table_canvas),
                "vscroll-policy", value);
        break;
    }
}

/**
 * e_table_get_next_row:
 * @e_table: The #ETable to query
 * @model_row: The model row to go from
 *
 * This function is used when your table is sorted, but you're using
 * model row numbers.  It returns the next row in sorted order as a model row.
 *
 * Return value:
 * The model row number.
 **/
gint
e_table_get_next_row (ETable *e_table,
                      gint model_row)
{
    g_return_val_if_fail (E_IS_TABLE (e_table), -1);

    if (e_table->sorter) {
        gint i;
        i = e_sorter_model_to_sorted (E_SORTER (e_table->sorter), model_row);
        i++;
        if (i < e_table_model_row_count (e_table->model)) {
            return e_sorter_sorted_to_model (E_SORTER (e_table->sorter), i);
        } else
            return -1;
    } else
        if (model_row < e_table_model_row_count (e_table->model) - 1)
            return model_row + 1;
        else
            return -1;
}

/**
 * e_table_get_prev_row:
 * @e_table: The #ETable to query
 * @model_row: The model row to go from
 *
 * This function is used when your table is sorted, but you're using
 * model row numbers.  It returns the previous row in sorted order as
 * a model row.
 *
 * Return value:
 * The model row number.
 **/
gint
e_table_get_prev_row (ETable *e_table,
                      gint model_row)
{
    g_return_val_if_fail (E_IS_TABLE (e_table), -1);

    if (e_table->sorter) {
        gint i;
        i = e_sorter_model_to_sorted (E_SORTER (e_table->sorter), model_row);
        i--;
        if (i >= 0)
            return e_sorter_sorted_to_model (E_SORTER (e_table->sorter), i);
        else
            return -1;
    } else
        return model_row - 1;
}

/**
 * e_table_model_to_view_row:
 * @e_table: The #ETable to query
 * @model_row: The model row number
 *
 * Turns a model row into a view row.
 *
 * Return value:
 * The view row number.
 **/
gint
e_table_model_to_view_row (ETable *e_table,
                           gint model_row)
{
    g_return_val_if_fail (E_IS_TABLE (e_table), -1);

    if (e_table->sorter)
        return e_sorter_model_to_sorted (E_SORTER (e_table->sorter), model_row);
    else
        return model_row;
}

/**
 * e_table_view_to_model_row:
 * @e_table: The #ETable to query
 * @view_row: The view row number
 *
 * Turns a view row into a model row.
 *
 * Return value:
 * The model row number.
 **/
gint
e_table_view_to_model_row (ETable *e_table,
                           gint view_row)
{
    g_return_val_if_fail (E_IS_TABLE (e_table), -1);

    if (e_table->sorter)
        return e_sorter_sorted_to_model (E_SORTER (e_table->sorter), view_row);
    else
        return view_row;
}

/**
 * e_table_get_cell_at:
 * @table: An #ETable widget
 * @x: X coordinate for the pixel
 * @y: Y coordinate for the pixel
 * @row_return: Pointer to return the row value
 * @col_return: Pointer to return the column value
 *
 * Return the row and column for the cell in which the pixel at (@x, @y) is
 * contained.
 **/
void
e_table_get_cell_at (ETable *table,
                     gint x,
                     gint y,
                     gint *row_return,
                     gint *col_return)
{
    GtkAdjustment *adjustment;
    GtkScrollable *scrollable;

    g_return_if_fail (E_IS_TABLE (table));
    g_return_if_fail (row_return != NULL);
    g_return_if_fail (col_return != NULL);

    /* FIXME it would be nice if it could handle a NULL row_return or
     * col_return gracefully.  */

    scrollable = GTK_SCROLLABLE (table->table_canvas);

    adjustment = gtk_scrollable_get_hadjustment (scrollable);
    x += gtk_adjustment_get_value (adjustment);

    adjustment = gtk_scrollable_get_vadjustment (scrollable);
    y += gtk_adjustment_get_value (adjustment);

    e_table_group_compute_location (
        table->group, &x, &y, row_return, col_return);
}

/**
 * e_table_get_cell_geometry:
 * @table: The #ETable.
 * @row: The row to get the geometry of.
 * @col: The col to get the geometry of.
 * @x_return: Returns the x coordinate of the upper left hand corner
 *            of the cell with respect to the widget.
 * @y_return: Returns the y coordinate of the upper left hand corner
 *            of the cell with respect to the widget.
 * @width_return: Returns the width of the cell.
 * @height_return: Returns the height of the cell.
 *
 * Returns the x, y, width, and height of the given cell.  These can
 * all be #NULL and they just won't be set.
 **/
void
e_table_get_cell_geometry (ETable *table,
                           gint row,
                           gint col,
                           gint *x_return,
                           gint *y_return,
                           gint *width_return,
                           gint *height_return)
{
    GtkAllocation allocation;
    GtkAdjustment *adjustment;
    GtkScrollable *scrollable;

    g_return_if_fail (E_IS_TABLE (table));

    scrollable = GTK_SCROLLABLE (table->table_canvas);

    e_table_group_get_cell_geometry (
        table->group, &row, &col, x_return, y_return,
        width_return, height_return);

    if (x_return && table->table_canvas) {
        adjustment = gtk_scrollable_get_hadjustment (scrollable);
        (*x_return) -= gtk_adjustment_get_value (adjustment);
    }

    if (y_return) {
        if (table->table_canvas) {
            adjustment = gtk_scrollable_get_vadjustment (scrollable);
            (*y_return) -= gtk_adjustment_get_value (adjustment);
        }

        if (table->header_canvas) {
            gtk_widget_get_allocation (
                GTK_WIDGET (table->header_canvas),
                &allocation);
            (*y_return) += allocation.height;
        }
    }
}

/**
 * e_table_get_mouse_over_cell:
 *
 * Similar to e_table_get_cell_at, only here we check
 * based on the mouse motion information in the group.
 **/
void
e_table_get_mouse_over_cell (ETable *table,
                             gint *row,
                             gint *col)
{
    g_return_if_fail (E_IS_TABLE (table));

    if (!table->group)
        return;

    e_table_group_get_mouse_over (table->group, row, col);
}

/**
 * e_table_get_selection_model:
 * @table: The #ETable to query
 *
 * Returns the table's #ESelectionModel in case you want to access it
 * directly.
 *
 * Return value:
 * The #ESelectionModel.
 **/
ESelectionModel *
e_table_get_selection_model (ETable *table)
{
    g_return_val_if_fail (E_IS_TABLE (table), NULL);

    return E_SELECTION_MODEL (table->selection);
}

struct _ETableDragSourceSite
{
    GdkModifierType    start_button_mask;
    GtkTargetList     *target_list;        /* Targets for drag data */
    GdkDragAction      actions;            /* Possible actions */
    GdkPixbuf         *pixbuf;             /* Icon for drag data */

    /* Stored button press information to detect drag beginning */
    gint               state;
    gint               x, y;
    gint               row, col;
};

typedef enum
{
  GTK_DRAG_STATUS_DRAG,
  GTK_DRAG_STATUS_WAIT,
  GTK_DRAG_STATUS_DROP
} GtkDragStatus;

typedef struct _GtkDragDestInfo GtkDragDestInfo;
typedef struct _GtkDragSourceInfo GtkDragSourceInfo;

struct _GtkDragDestInfo
{
  GtkWidget         *widget;       /* Widget in which drag is in */
  GdkDragContext    *context;      /* Drag context */
  GtkDragSourceInfo *proxy_source; /* Set if this is a proxy drag */
  GtkSelectionData  *proxy_data;   /* Set while retrieving proxied data */
  guint              dropped : 1;     /* Set after we receive a drop */
  guint32            proxy_drop_time; /* Timestamp for proxied drop */
  guint              proxy_drop_wait : 1; /* Set if we are waiting for a
                       * status reply before sending
                       * a proxied drop on.
                       */
  gint               drop_x, drop_y; /* Position of drop */
};

struct _GtkDragSourceInfo
{
  GtkWidget         *widget;
  GtkTargetList     *target_list; /* Targets for drag data */
  GdkDragAction      possible_actions; /* Actions allowed by source */
  GdkDragContext    *context;     /* drag context */
  GtkWidget         *icon_window; /* Window for drag */
  GtkWidget         *ipc_widget;  /* GtkInvisible for grab, message passing */
  GdkCursor         *cursor;      /* Cursor for drag */
  gint hot_x, hot_y;          /* Hot spot for drag */
  gint button;            /* mouse button starting drag */

  GtkDragStatus      status;      /* drag status */
  GdkEvent          *last_event;  /* motion event waiting for response */

  gint               start_x, start_y; /* Initial position */
  gint               cur_x, cur_y;     /* Current Position */

  GList             *selections;  /* selections we've claimed */

  GtkDragDestInfo   *proxy_dest;  /* Set if this is a proxy drag */

  guint              drop_timeout;     /* Timeout for aborting drop */
  guint              destroy_icon : 1; /* If true, destroy icon_window
                    */
};

/* Drag & drop stuff. */
/* Target */

/**
 * e_table_drag_get_data:
 * @table:
 * @row:
 * @col:
 * @context:
 * @target:
 * @time:
 *
 *
 **/
void
e_table_drag_get_data (ETable *table,
                       gint row,
                       gint col,
                       GdkDragContext *context,
                       GdkAtom target,
                       guint32 time)
{
    g_return_if_fail (E_IS_TABLE (table));

    gtk_drag_get_data (
        GTK_WIDGET (table),
        context,
        target,
        time);
}

/**
 * e_table_drag_highlight:
 * @table: The #ETable to highlight
 * @row: The row number of the cell to highlight
 * @col: The column number of the cell to highlight
 *
 * Set col to -1 to highlight the entire row.  If row is -1, this is
 * identical to e_table_drag_unhighlight().
 **/
void
e_table_drag_highlight (ETable *table,
                        gint row,
                        gint col)
{
    GtkAllocation allocation;
    GtkAdjustment *adjustment;
    GtkScrollable *scrollable;
    GtkStyle *style;

    g_return_if_fail (E_IS_TABLE (table));

    scrollable = GTK_SCROLLABLE (table->table_canvas);
    style = gtk_widget_get_style (GTK_WIDGET (table));
    gtk_widget_get_allocation (GTK_WIDGET (scrollable), &allocation);

    if (row != -1) {
        gint x, y, width, height;
        if (col == -1) {
            e_table_get_cell_geometry (table, row, 0, &x, &y, &width, &height);
            x = 0;
            width = allocation.width;
        } else {
            e_table_get_cell_geometry (table, row, col, &x, &y, &width, &height);
            adjustment = gtk_scrollable_get_hadjustment (scrollable);
            x += gtk_adjustment_get_value (adjustment);
        }

        adjustment = gtk_scrollable_get_vadjustment (scrollable);
        y += gtk_adjustment_get_value (adjustment);

        if (table->drop_highlight == NULL) {
            table->drop_highlight = gnome_canvas_item_new (
                gnome_canvas_root (table->table_canvas),
                gnome_canvas_rect_get_type (),
                "fill_color", NULL,
                "outline_color_gdk", &style->fg[GTK_STATE_NORMAL],
                NULL);
        }
        gnome_canvas_item_set (
            table->drop_highlight,
            "x1", (gdouble) x,
            "x2", (gdouble) x + width - 1,
            "y1", (gdouble) y,
            "y2", (gdouble) y + height - 1,
            NULL);
    } else {
        if (table->drop_highlight) {
            g_object_run_dispose (G_OBJECT (table->drop_highlight));
            table->drop_highlight = NULL;
        }
    }
}

/**
 * e_table_drag_unhighlight:
 * @table: The #ETable to unhighlight
 *
 * Removes the highlight from an #ETable.
 **/
void
e_table_drag_unhighlight (ETable *table)
{
    g_return_if_fail (E_IS_TABLE (table));

    if (table->drop_highlight) {
        g_object_run_dispose (G_OBJECT (table->drop_highlight));
        table->drop_highlight = NULL;
    }
}

void
e_table_drag_dest_set (ETable *table,
                       GtkDestDefaults flags,
                       const GtkTargetEntry *targets,
                       gint n_targets,
                       GdkDragAction actions)
{
    g_return_if_fail (E_IS_TABLE (table));

    gtk_drag_dest_set (
        GTK_WIDGET (table), flags, targets, n_targets, actions);
}

void
e_table_drag_dest_set_proxy (ETable *table,
                             GdkWindow *proxy_window,
                             GdkDragProtocol protocol,
                             gboolean use_coordinates)
{
    g_return_if_fail (E_IS_TABLE (table));

    gtk_drag_dest_set_proxy (
        GTK_WIDGET (table), proxy_window, protocol, use_coordinates);
}

/*
 * There probably should be functions for setting the targets
 * as a GtkTargetList
 */

void
e_table_drag_dest_unset (GtkWidget *widget)
{
    g_return_if_fail (E_IS_TABLE (widget));

    gtk_drag_dest_unset (widget);
}

/* Source side */

static gint
et_real_start_drag (ETable *table,
                    gint row,
                    gint col,
                    GdkEvent *event)
{
    GtkDragSourceInfo *info;
    GdkDragContext *context;
    ETableDragSourceSite *site;

    if (table->do_drag) {
        site = table->site;

        site->state = 0;
        context = e_table_drag_begin (
            table, row, col,
            site->target_list,
            site->actions,
            1, event);

        if (context) {
            info = g_dataset_get_data (context, "gtk-info");

            if (info && !info->icon_window) {
                if (site->pixbuf)
                    gtk_drag_set_icon_pixbuf (
                        context,
                        site->pixbuf,
                        -2, -2);
                else
                    gtk_drag_set_icon_default (context);
            }
        }
        return TRUE;
    }
    return FALSE;
}

/**
 * e_table_drag_source_set:
 * @table: The #ETable to set up as a drag site
 * @start_button_mask: Mask of allowed buttons to start drag
 * @targets: Table of targets for this source
 * @n_targets: Number of targets in @targets
 * @actions: Actions allowed for this source
 *
 * Registers this table as a drag site, and possibly adds default behaviors.
 **/
void
e_table_drag_source_set (ETable *table,
                         GdkModifierType start_button_mask,
                         const GtkTargetEntry *targets,
                         gint n_targets,
                         GdkDragAction actions)
{
    ETableDragSourceSite *site;
    GtkWidget *canvas;

    g_return_if_fail (E_IS_TABLE (table));

    canvas = GTK_WIDGET (table->table_canvas);
    site = table->site;

    gtk_widget_add_events (
        canvas,
        gtk_widget_get_events (canvas) |
        GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
        GDK_BUTTON_MOTION_MASK | GDK_STRUCTURE_MASK);

    table->do_drag = TRUE;

    if (site) {
        if (site->target_list)
            gtk_target_list_unref (site->target_list);
    } else {
        site = g_new0 (ETableDragSourceSite, 1);
        table->site = site;
    }

    site->start_button_mask = start_button_mask;

    if (targets)
        site->target_list = gtk_target_list_new (targets, n_targets);
    else
        site->target_list = NULL;

    site->actions = actions;
}

/**
 * e_table_drag_source_unset:
 * @table: The #ETable to un set up as a drag site
 *
 * Unregisters this #ETable as a drag site.
 **/
void
e_table_drag_source_unset (ETable *table)
{
    ETableDragSourceSite *site;

    g_return_if_fail (E_IS_TABLE (table));

    site = table->site;

    if (site) {
        if (site->target_list)
            gtk_target_list_unref (site->target_list);
        g_free (site);
        table->site = NULL;
    }
    table->do_drag = FALSE;
}

/* There probably should be functions for setting the targets
 * as a GtkTargetList
 */

/**
 * e_table_drag_begin:
 * @table: The #ETable to drag from
 * @row: The row number of the cell
 * @col: The col number of the cell
 * @targets: The list of targets supported by the drag
 * @actions: The available actions supported by the drag
 * @button: The button held down for the drag
 * @event: The event that initiated the drag
 *
 * Start a drag from this cell.
 *
 * Return value:
 * The drag context.
 **/
GdkDragContext *
e_table_drag_begin (ETable *table,
                    gint row,
                    gint col,
                    GtkTargetList *targets,
                    GdkDragAction actions,
                    gint button,
                    GdkEvent *event)
{
    g_return_val_if_fail (E_IS_TABLE (table), NULL);

    table->drag_row = row;
    table->drag_col = col;

    return gtk_drag_begin (
        GTK_WIDGET (table), targets, actions, button, event);
}

static void
et_drag_begin (GtkWidget *widget,
               GdkDragContext *context,
               ETable *et)
{
    g_signal_emit (
        et, et_signals[TABLE_DRAG_BEGIN], 0,
        et->drag_row, et->drag_col, context);
}

static void
et_drag_end (GtkWidget *widget,
             GdkDragContext *context,
             ETable *et)
{
    g_signal_emit (
        et, et_signals[TABLE_DRAG_END], 0,
        et->drag_row, et->drag_col, context);
}

static void
et_drag_data_get (GtkWidget *widget,
                  GdkDragContext *context,
                  GtkSelectionData *selection_data,
                  guint info,
                  guint time,
                  ETable *et)
{
    g_signal_emit (
        et, et_signals[TABLE_DRAG_DATA_GET], 0,
        et->drag_row, et->drag_col, context, selection_data,
        info, time);
}

static void
et_drag_data_delete (GtkWidget *widget,
                     GdkDragContext *context,
                     ETable *et)
{
    g_signal_emit (
        et, et_signals[TABLE_DRAG_DATA_DELETE], 0,
        et->drag_row, et->drag_col, context);
}

static gboolean
do_drag_motion (ETable *et,
                GdkDragContext *context,
                gint x,
                gint y,
                guint time)
{
    gboolean ret_val;
    gint row = -1, col = -1;

    e_table_get_cell_at (et, x, y, &row, &col);

    if (row != et->drop_row && col != et->drop_row) {
        g_signal_emit (
            et, et_signals[TABLE_DRAG_LEAVE], 0,
            et->drop_row, et->drop_col, context, time);
    }

    et->drop_row = row;
    et->drop_col = col;

    g_signal_emit (
        et, et_signals[TABLE_DRAG_MOTION], 0,
        et->drop_row, et->drop_col, context, x, y, time, &ret_val);

    return ret_val;
}

static gboolean
scroll_timeout (gpointer data)
{
    ETable *et = data;
    gint dx = 0, dy = 0;
    GtkAdjustment *adjustment;
    GtkScrollable *scrollable;
    gdouble old_h_value;
    gdouble new_h_value;
    gdouble old_v_value;
    gdouble new_v_value;
    gdouble page_size;
    gdouble lower;
    gdouble upper;

    if (et->scroll_direction & ET_SCROLL_DOWN)
        dy += 20;
    if (et->scroll_direction & ET_SCROLL_UP)
        dy -= 20;

    if (et->scroll_direction & ET_SCROLL_RIGHT)
        dx += 20;
    if (et->scroll_direction & ET_SCROLL_LEFT)
        dx -= 20;

    scrollable = GTK_SCROLLABLE (et->table_canvas);

    adjustment = gtk_scrollable_get_hadjustment (scrollable);

    lower = gtk_adjustment_get_lower (adjustment);
    upper = gtk_adjustment_get_upper (adjustment);
    page_size = gtk_adjustment_get_page_size (adjustment);

    old_h_value = gtk_adjustment_get_value (adjustment);
    new_h_value = CLAMP (old_h_value + dx, lower, upper - page_size);

    gtk_adjustment_set_value (adjustment, new_h_value);

    adjustment = gtk_scrollable_get_vadjustment (scrollable);

    lower = gtk_adjustment_get_lower (adjustment);
    upper = gtk_adjustment_get_upper (adjustment);
    page_size = gtk_adjustment_get_page_size (adjustment);

    old_v_value = gtk_adjustment_get_value (adjustment);
    new_v_value = CLAMP (old_v_value + dy, lower, upper - page_size);

    gtk_adjustment_set_value (adjustment, new_v_value);

    if (new_h_value != old_h_value || new_v_value != old_v_value)
        do_drag_motion (
            et,
            et->last_drop_context,
            et->last_drop_x,
            et->last_drop_y,
            et->last_drop_time);

    return TRUE;
}

static void
scroll_on (ETable *et,
           guint scroll_direction)
{
    if (et->scroll_idle_id == 0 || scroll_direction != et->scroll_direction) {
        if (et->scroll_idle_id != 0)
            g_source_remove (et->scroll_idle_id);
        et->scroll_direction = scroll_direction;
        et->scroll_idle_id = g_timeout_add (100, scroll_timeout, et);
    }
}

static void
scroll_off (ETable *et)
{
    if (et->scroll_idle_id) {
        g_source_remove (et->scroll_idle_id);
        et->scroll_idle_id = 0;
    }
}

static void
context_destroyed (gpointer data)
{
    ETable *et = data;
    /* if (!G_OBJECT_DESTROYED (et)) */
/* FIXME: */
    {
        et->last_drop_x       = 0;
        et->last_drop_y       = 0;
        et->last_drop_time    = 0;
        et->last_drop_context = NULL;
        scroll_off (et);
    }
    g_object_unref (et);
}

static void
context_connect (ETable *et,
                 GdkDragContext *context)
{
    if (g_dataset_get_data (context, "e-table") == NULL) {
        g_object_ref (et);
        g_dataset_set_data_full (context, "e-table", et, context_destroyed);
    }
}

static void
et_drag_leave (GtkWidget *widget,
               GdkDragContext *context,
               guint time,
               ETable *et)
{
    g_signal_emit (
        et, et_signals[TABLE_DRAG_LEAVE], 0,
        et->drop_row, et->drop_col, context, time);

    et->drop_row = -1;
    et->drop_col = -1;

    scroll_off (et);
}

static gboolean
et_drag_motion (GtkWidget *widget,
                GdkDragContext *context,
                gint x,
                gint y,
                guint time,
                ETable *et)
{
    GtkAllocation allocation;
    gboolean ret_val;
    guint direction = 0;

    gtk_widget_get_allocation (widget, &allocation);

    et->last_drop_x = x;
    et->last_drop_y = y;
    et->last_drop_time = time;
    et->last_drop_context = context;
    context_connect (et, context);

    ret_val = do_drag_motion (et, context, x, y, time);

    if (y < 20)
        direction |= ET_SCROLL_UP;
    if (y > allocation.height - 20)
        direction |= ET_SCROLL_DOWN;
    if (x < 20)
        direction |= ET_SCROLL_LEFT;
    if (x > allocation.width - 20)
        direction |= ET_SCROLL_RIGHT;

    if (direction != 0)
        scroll_on (et, direction);
    else
        scroll_off (et);

    return ret_val;
}

static gboolean
et_drag_drop (GtkWidget *widget,
              GdkDragContext *context,
              gint x,
              gint y,
              guint time,
              ETable *et)
{
    gboolean ret_val;
    gint row, col;

    e_table_get_cell_at (et, x, y, &row, &col);

    if (row != et->drop_row && col != et->drop_row) {
        g_signal_emit (
            et, et_signals[TABLE_DRAG_LEAVE], 0,
            et->drop_row, et->drop_col, context, time);
        g_signal_emit (
            et, et_signals[TABLE_DRAG_MOTION], 0,
            row, col, context, x, y, time, &ret_val);
    }
    et->drop_row = row;
    et->drop_col = col;
    g_signal_emit (
        et, et_signals[TABLE_DRAG_DROP], 0,
        et->drop_row, et->drop_col, context, x, y, time, &ret_val);
    et->drop_row = -1;
    et->drop_col = -1;

    scroll_off (et);

    return ret_val;
}

static void
et_drag_data_received (GtkWidget *widget,
                       GdkDragContext *context,
                       gint x,
                       gint y,
                       GtkSelectionData *selection_data,
                       guint info,
                       guint time,
                       ETable *et)
{
    gint row, col;

    e_table_get_cell_at (et, x, y, &row, &col);

    g_signal_emit (
        et, et_signals[TABLE_DRAG_DATA_RECEIVED], 0,
        row, col, context, x, y, selection_data, info, time);
}

static void
e_table_class_init (ETableClass *class)
{
    GObjectClass *object_class;
    GtkWidgetClass *widget_class;

    object_class                    = (GObjectClass *) class;
    widget_class                    = (GtkWidgetClass *) class;

    object_class->dispose           = et_dispose;
    object_class->finalize          = et_finalize;
    object_class->set_property      = et_set_property;
    object_class->get_property      = et_get_property;

    widget_class->grab_focus        = et_grab_focus;
    widget_class->unrealize         = et_unrealize;
    widget_class->get_preferred_width = et_get_preferred_width;
    widget_class->get_preferred_height = et_get_preferred_height;

    widget_class->focus             = et_focus;

    class->cursor_change            = NULL;
    class->cursor_activated         = NULL;
    class->selection_change         = NULL;
    class->double_click             = NULL;
    class->right_click              = NULL;
    class->click                    = NULL;
    class->key_press                = NULL;
    class->start_drag               = et_real_start_drag;
    class->state_change             = NULL;
    class->white_space_event        = NULL;

    class->table_drag_begin         = NULL;
    class->table_drag_end           = NULL;
    class->table_drag_data_get      = NULL;
    class->table_drag_data_delete   = NULL;

    class->table_drag_leave         = NULL;
    class->table_drag_motion        = NULL;
    class->table_drag_drop          = NULL;
    class->table_drag_data_received = NULL;

    et_signals[CURSOR_CHANGE] = g_signal_new (
        "cursor_change",
        G_OBJECT_CLASS_TYPE (object_class),
        G_SIGNAL_RUN_LAST,
        G_STRUCT_OFFSET (ETableClass, cursor_change),
        NULL, NULL,
        g_cclosure_marshal_VOID__INT,
        G_TYPE_NONE, 1, G_TYPE_INT);

    et_signals[CURSOR_ACTIVATED] = g_signal_new (
        "cursor_activated",
        G_OBJECT_CLASS_TYPE (object_class),
        G_SIGNAL_RUN_LAST,
        G_STRUCT_OFFSET (ETableClass, cursor_activated),
        NULL, NULL,
        g_cclosure_marshal_VOID__INT,
        G_TYPE_NONE, 1, G_TYPE_INT);

    et_signals[SELECTION_CHANGE] = g_signal_new (
        "selection_change",
        G_OBJECT_CLASS_TYPE (object_class),
        G_SIGNAL_RUN_LAST,
        G_STRUCT_OFFSET (ETableClass, selection_change),
        NULL, NULL,
        g_cclosure_marshal_VOID__VOID,
        G_TYPE_NONE, 0);

    et_signals[DOUBLE_CLICK] = g_signal_new (
        "double_click",
        G_OBJECT_CLASS_TYPE (object_class),
        G_SIGNAL_RUN_LAST,
        G_STRUCT_OFFSET (ETableClass, double_click),
        NULL, NULL,
        e_marshal_NONE__INT_INT_BOXED,
        G_TYPE_NONE, 3,
        G_TYPE_INT,
        G_TYPE_INT,
        GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE);

    et_signals[RIGHT_CLICK] = g_signal_new (
        "right_click",
        G_OBJECT_CLASS_TYPE (object_class),
        G_SIGNAL_RUN_LAST,
        G_STRUCT_OFFSET (ETableClass, right_click),
        g_signal_accumulator_true_handled, NULL,
        e_marshal_BOOLEAN__INT_INT_BOXED,
        G_TYPE_BOOLEAN, 3,
        G_TYPE_INT,
        G_TYPE_INT,
        GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE);

    et_signals[CLICK] = g_signal_new (
        "click",
        G_OBJECT_CLASS_TYPE (object_class),
        G_SIGNAL_RUN_LAST,
        G_STRUCT_OFFSET (ETableClass, click),
        g_signal_accumulator_true_handled, NULL,
        e_marshal_BOOLEAN__INT_INT_BOXED,
        G_TYPE_BOOLEAN, 3,
        G_TYPE_INT,
        G_TYPE_INT,
        GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE);

    et_signals[KEY_PRESS] = g_signal_new (
        "key_press",
        G_OBJECT_CLASS_TYPE (object_class),
        G_SIGNAL_RUN_LAST,
        G_STRUCT_OFFSET (ETableClass, key_press),
        g_signal_accumulator_true_handled, NULL,
        e_marshal_BOOLEAN__INT_INT_BOXED,
        G_TYPE_BOOLEAN, 3,
        G_TYPE_INT,
        G_TYPE_INT,
        GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE);

    et_signals[START_DRAG] = g_signal_new (
        "start_drag",
        G_OBJECT_CLASS_TYPE (object_class),
        G_SIGNAL_RUN_LAST,
        G_STRUCT_OFFSET (ETableClass, start_drag),
        g_signal_accumulator_true_handled, NULL,
        e_marshal_BOOLEAN__INT_INT_BOXED,
        G_TYPE_BOOLEAN, 3,
        G_TYPE_INT,
        G_TYPE_INT,
        GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE);

    et_signals[STATE_CHANGE] = g_signal_new (
        "state_change",
        G_OBJECT_CLASS_TYPE (object_class),
        G_SIGNAL_RUN_LAST,
        G_STRUCT_OFFSET (ETableClass, state_change),
        NULL, NULL,
        g_cclosure_marshal_VOID__VOID,
        G_TYPE_NONE, 0);

    et_signals[WHITE_SPACE_EVENT] = g_signal_new (
        "white_space_event",
        G_OBJECT_CLASS_TYPE (object_class),
        G_SIGNAL_RUN_LAST,
        G_STRUCT_OFFSET (ETableClass, white_space_event),
        g_signal_accumulator_true_handled, NULL,
        e_marshal_BOOLEAN__BOXED,
        G_TYPE_BOOLEAN, 1,
        GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE);

    et_signals[TABLE_DRAG_BEGIN] = g_signal_new (
        "table_drag_begin",
        G_OBJECT_CLASS_TYPE (object_class),
        G_SIGNAL_RUN_LAST,
        G_STRUCT_OFFSET (ETableClass, table_drag_begin),
        NULL, NULL,
        e_marshal_NONE__INT_INT_OBJECT,
        G_TYPE_NONE, 3,
        G_TYPE_INT,
        G_TYPE_INT,
        GDK_TYPE_DRAG_CONTEXT);

    et_signals[TABLE_DRAG_END] = g_signal_new (
        "table_drag_end",
        G_OBJECT_CLASS_TYPE (object_class),
        G_SIGNAL_RUN_LAST,
        G_STRUCT_OFFSET (ETableClass, table_drag_end),
        NULL, NULL,
        e_marshal_NONE__INT_INT_OBJECT,
        G_TYPE_NONE, 3,
        G_TYPE_INT,
        G_TYPE_INT,
        GDK_TYPE_DRAG_CONTEXT);

    et_signals[TABLE_DRAG_DATA_GET] = g_signal_new (
        "table_drag_data_get",
        G_OBJECT_CLASS_TYPE (object_class),
        G_SIGNAL_RUN_LAST,
        G_STRUCT_OFFSET (ETableClass, table_drag_data_get),
        NULL, NULL,
        e_marshal_NONE__INT_INT_OBJECT_BOXED_UINT_UINT,
        G_TYPE_NONE, 6,
        G_TYPE_INT,
        G_TYPE_INT,
        GDK_TYPE_DRAG_CONTEXT,
        GTK_TYPE_SELECTION_DATA | G_SIGNAL_TYPE_STATIC_SCOPE,
        G_TYPE_UINT,
        G_TYPE_UINT);

    et_signals[TABLE_DRAG_DATA_DELETE] = g_signal_new (
        "table_drag_data_delete",
        G_OBJECT_CLASS_TYPE (object_class),
        G_SIGNAL_RUN_LAST,
        G_STRUCT_OFFSET (ETableClass, table_drag_data_delete),
        NULL, NULL,
        e_marshal_NONE__INT_INT_OBJECT,
        G_TYPE_NONE, 3,
        G_TYPE_INT,
        G_TYPE_INT,
        GDK_TYPE_DRAG_CONTEXT);

    et_signals[TABLE_DRAG_LEAVE] = g_signal_new (
        "table_drag_leave",
        G_OBJECT_CLASS_TYPE (object_class),
        G_SIGNAL_RUN_LAST,
        G_STRUCT_OFFSET (ETableClass, table_drag_leave),
        NULL, NULL,
        e_marshal_NONE__INT_INT_OBJECT_UINT,
        G_TYPE_NONE, 4,
        G_TYPE_INT,
        G_TYPE_INT,
        GDK_TYPE_DRAG_CONTEXT,
        G_TYPE_UINT);

    et_signals[TABLE_DRAG_MOTION] = g_signal_new (
        "table_drag_motion",
        G_OBJECT_CLASS_TYPE (object_class),
        G_SIGNAL_RUN_LAST,
        G_STRUCT_OFFSET (ETableClass, table_drag_motion),
        NULL, NULL,
        e_marshal_BOOLEAN__INT_INT_OBJECT_INT_INT_UINT,
        G_TYPE_BOOLEAN, 6,
        G_TYPE_INT,
        G_TYPE_INT,
        GDK_TYPE_DRAG_CONTEXT,
        G_TYPE_INT,
        G_TYPE_INT,
        G_TYPE_UINT);

    et_signals[TABLE_DRAG_DROP] = g_signal_new (
        "table_drag_drop",
        G_OBJECT_CLASS_TYPE (object_class),
        G_SIGNAL_RUN_LAST,
        G_STRUCT_OFFSET (ETableClass, table_drag_drop),
        NULL, NULL,
        e_marshal_BOOLEAN__INT_INT_OBJECT_INT_INT_UINT,
        G_TYPE_BOOLEAN, 6,
        G_TYPE_INT,
        G_TYPE_INT,
        GDK_TYPE_DRAG_CONTEXT,
        G_TYPE_INT,
        G_TYPE_INT,
        G_TYPE_UINT);

    et_signals[TABLE_DRAG_DATA_RECEIVED] = g_signal_new (
        "table_drag_data_received",
        G_OBJECT_CLASS_TYPE (object_class),
        G_SIGNAL_RUN_LAST,
        G_STRUCT_OFFSET (ETableClass, table_drag_data_received),
        NULL, NULL,
        e_marshal_NONE__INT_INT_OBJECT_INT_INT_BOXED_UINT_UINT,
        G_TYPE_NONE, 8,
        G_TYPE_INT,
        G_TYPE_INT,
        GDK_TYPE_DRAG_CONTEXT,
        G_TYPE_INT,
        G_TYPE_INT,
        GTK_TYPE_SELECTION_DATA | G_SIGNAL_TYPE_STATIC_SCOPE,
        G_TYPE_UINT,
        G_TYPE_UINT);

    g_object_class_install_property (
        object_class,
        PROP_LENGTH_THRESHOLD,
        g_param_spec_int (
            "length_threshold",
            "Length Threshold",
            NULL,
            0, G_MAXINT, 0,
            G_PARAM_WRITABLE));

    g_object_class_install_property (
        object_class,
        PROP_UNIFORM_ROW_HEIGHT,
        g_param_spec_boolean (
            "uniform_row_height",
            "Uniform row height",
            NULL,
            FALSE,
            G_PARAM_READWRITE));

    g_object_class_install_property (
        object_class,
        PROP_ALWAYS_SEARCH,
        g_param_spec_boolean (
            "always_search",
            "Always search",
            NULL,
            FALSE,
            G_PARAM_READWRITE));

    g_object_class_install_property (
        object_class,
        PROP_USE_CLICK_TO_ADD,
        g_param_spec_boolean (
            "use_click_to_add",
            "Use click to add",
            NULL,
            FALSE,
            G_PARAM_READWRITE));

    g_object_class_install_property (
        object_class,
        PROP_MODEL,
        g_param_spec_object (
            "model",
            "Model",
            NULL,
            E_TYPE_TABLE_MODEL,
            G_PARAM_READABLE));

    g_object_class_install_property (
        object_class,
        PROP_IS_EDITING,
        g_param_spec_boolean (
            "is-editing",
            "Whether is in an editing mode",
            "Whether is in an editing mode",
            FALSE,
            G_PARAM_READABLE));

    gtk_widget_class_install_style_property (
        widget_class,
        g_param_spec_int (
            "vertical-spacing",
            "Vertical Row Spacing",
            "Vertical space between rows. "
            "It is added to top and to bottom of a row",
            0, G_MAXINT, 3,
            G_PARAM_READABLE |
            G_PARAM_STATIC_STRINGS));

    /* Scrollable interface */
    g_object_class_override_property (
        object_class, PROP_HADJUSTMENT, "hadjustment");
    g_object_class_override_property (
        object_class, PROP_VADJUSTMENT, "vadjustment");
    g_object_class_override_property (
        object_class, PROP_HSCROLL_POLICY, "hscroll-policy");
    g_object_class_override_property (
        object_class, PROP_VSCROLL_POLICY, "vscroll-policy");

    gal_a11y_e_table_init ();
}

void
e_table_freeze_state_change (ETable *table)
{
    g_return_if_fail (table != NULL);

    table->state_change_freeze++;
    if (table->state_change_freeze == 1)
        table->state_changed = FALSE;

    g_return_if_fail (table->state_change_freeze != 0);
}

void
e_table_thaw_state_change (ETable *table)
{
    g_return_if_fail (table != NULL);
    g_return_if_fail (table->state_change_freeze != 0);

    table->state_change_freeze--;
    if (table->state_change_freeze == 0 && table->state_changed) {
        table->state_changed = FALSE;
        e_table_state_change (table);
    }
}

gboolean
e_table_is_editing (ETable *table)
{
    g_return_val_if_fail (E_IS_TABLE (table), FALSE);

    return (table->click_to_add && e_table_click_to_add_is_editing (E_TABLE_CLICK_TO_ADD (table->click_to_add))) ||
           (table->group && e_table_group_is_editing (table->group));
}