Merge branch 'master' of https://github.com/jgoodman/Freeside
[freeside.git] / rt / share / html / Admin / Tools / Theme.html
1 %# BEGIN BPS TAGGED BLOCK {{{
2 %#
3 %# COPYRIGHT:
4 %#
5 %# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
6 %#                                          <sales@bestpractical.com>
7 %#
8 %# (Except where explicitly superseded by other copyright notices)
9 %#
10 %#
11 %# LICENSE:
12 %#
13 %# This work is made available to you under the terms of Version 2 of
14 %# the GNU General Public License. A copy of that license should have
15 %# been provided with this software, but in any event can be snarfed
16 %# from www.gnu.org.
17 %#
18 %# This work is distributed in the hope that it will be useful, but
19 %# WITHOUT ANY WARRANTY; without even the implied warranty of
20 %# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 %# General Public License for more details.
22 %#
23 %# You should have received a copy of the GNU General Public License
24 %# along with this program; if not, write to the Free Software
25 %# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 %# 02110-1301 or visit their web page on the internet at
27 %# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28 %#
29 %#
30 %# CONTRIBUTION SUBMISSION POLICY:
31 %#
32 %# (The following paragraph is not intended to limit the rights granted
33 %# to you to modify and distribute this software under the terms of
34 %# the GNU General Public License and is only of importance to you if
35 %# you choose to contribute your changes and enhancements to the
36 %# community by submitting them to Best Practical Solutions, LLC.)
37 %#
38 %# By intentionally submitting any modifications, corrections or
39 %# derivatives to this work, or any other work intended for use with
40 %# Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 %# you are the copyright holder for those contributions and you grant
42 %# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 %# royalty-free, perpetual, license to use, copy, create derivative
44 %# works based on those contributions, and sublicense and distribute
45 %# those contributions and any derivatives thereof.
46 %#
47 %# END BPS TAGGED BLOCK }}}
48 <& /Admin/Elements/Header,
49     Title => loc("Theme"),
50 &>
51 <& /Elements/Tabs &>
52 <& /Elements/ListActions, actions => \@results &>
53
54 <script type="text/javascript" src="<%RT->Config->Get('WebPath')%>/NoAuth/js/farbtastic.js"></script>
55
56 <div id="simple-customize">
57 <div id="upload-logo">
58   <h2>Logo</h2>
59   <& /Elements/Logo, id => 'logo-theme-editor', ShowName => 0 &>
60   <form method="POST" enctype="multipart/form-data">
61     <label for="logo-upload"><&|/l&>Upload a new logo</&>:</label>
62     <input type="file" name="logo-upload" id="logo-upload" /><br />
63     <div class="gd-support">
64 % if (%gd_can) {
65     <&|/l, $valid_image_types &>Your system supports automatic color suggestions for: [_1]</&>
66 % } else {
67     <&|/l&>GD is disabled or not installed. You can upload an image, but you won't get automatic color suggestions.</&>
68 % }
69     </div>
70     <input name="reset_logo" value="Reset to default RT Logo" type="submit" />
71     <input type="submit" value="Upload" />
72   </form>
73 </div>
74
75 <div id="customize-theme">
76   <h2>Customize the RT theme</h2>
77   <ol>
78     <li>
79       <label for="section"><&|/l&>Select a section</&>:</label>
80       <select id="section"></select>
81     </li>
82     <li>
83       <div class="description"><&|/l&>Select a color for the section</&>:</div>
84 % if ($colors) {
85 <div class="primary-colors">
86 %   for (@$colors) {
87 %     my $fg = $_->{l} >= $text_threshold ? 'black' : 'white';
88 <button type="button" class="color-template"
89         style="background-color: rgb(<% $_->{c} %>); color: <% $fg %>;">
90   <&|/l&>Text</&>
91 </button>
92 %   }
93 </div>
94 % }
95       <div id="color-picker"></div>
96     </li>
97   </ol>
98 </div>
99 </div>
100
101 <div id="custom-css">
102   <h2>Custom CSS (Advanced)</h2>
103   
104   <form method="POST">
105     <textarea rows=20 id="user_css" name="user_css" wrap="off"><% $user_css %></textarea><br />
106     <input id="try" type="button" class="button" value="Try" />
107     <input id="reset" type="reset" value="Reset" type="submit" />
108     <input name="reset_css" value="Reset to default RT Theme" type="submit" />
109     <input value="Save" type="submit" />
110   </form>
111 </div>
112
113 <%ONCE>
114 my @sections = (
115     ['Page'         => ['body']],
116     ['Header'       => ['div#quickbar', 'body.aileron #main-navigation #app-nav > li, body.aileron #main-navigation #app-nav > li > a, #prefs-menu > li, #prefs-menu > li > a, #logo .rtname']],
117     ['Page title'   => ['div#header h1']],
118     ['Page content' => ['div#body']],
119     ['Buttons'      => ['input[type="reset"], input[type="submit"], input[class="button"]']],
120     ['Button hover' => ['input[type="reset"]:hover, input[type="submit"]:hover, input[class="button"]:hover']],
121 );
122 </%ONCE>
123 <script type="text/javascript">
124 var section_css_mapping = <% JSON(\@sections) |n%>;
125
126 jQuery(function($) {
127
128     jQuery.each(section_css_mapping, function(i,v){
129         $('select#section').append($("<option/>")
130                            .attr('value', v[0])
131                            .text(v[0]));
132     });
133
134     function update_sitecss(text) {
135         if (!text)
136             text = $('#user_css').val();
137
138         // IE 8 doesn't let us update the innerHTML of <style> tags (with jQuery.text())
139         // see: http://stackoverflow.com/questions/2692770/style-style-textcss-appendtohead-does-not-work-in-ie/2692861#2692861
140         $("style#sitecss").remove();
141         $("<style id='sitecss' type='text/css' media='all'>" + text + "</style>").appendTo('head');
142     }
143
144     update_sitecss();
145     $('#try').click(function() {
146         update_sitecss();
147     });
148
149     $('#reset').click(function() {
150         setTimeout(function() {
151             update_sitecss();
152         }, 1000);
153     });
154
155     function change_color(bg, fg) {
156       var section = $('select#section').val();
157
158       var applying = jQuery.grep(section_css_mapping, function(a){ return a[0] == section })[0][1];
159       var css = $('#user_css').val();
160       if (applying) {
161           var specials = new RegExp("([.*+?|()\\[\\]{}\\\\])", "g");
162           for (var name in applying) {
163               var selector = (applying[name]).replace(specials, "\\$1");
164               var rule = new RegExp('^'+selector+'\\s*\{.*?\}', "m");
165               var newcss = "background: " + bg;
166
167               /* Don't set the text color on <body> as it affects too much */
168               if (applying[name] != "body")
169                   newcss += "; color: " + fg;
170
171               /* Kill the border on the quickbar if we're styling it */
172               if (applying[name].match(/quickbar/))
173                   newcss += "; border: none;"
174
175               /* Page title's text color is the selected color */
176               if (applying[name].match(/#header/))
177                   newcss = "color: " + bg;
178
179               /* Nav doesn't need a background, but it wants text color */
180               if (applying[name].match(/#main-navigation/))
181                   newcss = "color: " + fg;
182
183               css = css.replace(rule, applying[name]+" { "+newcss+" }");
184           }
185       }
186       $('#user_css').val(css);
187       update_sitecss(css);
188     }
189
190     $('#color-picker').farbtastic(function(color){ change_color(color, this.hsl[2] > <% $text_threshold %> ? '#000' : '#fff') });
191
192     $('button.color-template').click(function() {
193       change_color($(this).css('background-color'), $(this).css('color'));
194   });
195
196
197 });
198 </script>
199 <%INIT>
200 unless ($session{'CurrentUser'}->HasRight( Object=> RT->System, Right => 'SuperUser')) {
201     Abort(loc('This feature is only available to system administrators.'));
202 }
203
204 use Digest::MD5 'md5_hex';
205
206 my $text_threshold = 0.6;
207 my @results;
208 my $imgdata;
209
210 if (my $file_hash = _UploadedFile( 'logo-upload' )) {
211     my ($id, $msg) = RT->System->SetAttribute( Name => "UserLogo",
212                                                 Description => "User-provided logo",
213                                                 Content => {
214                                                     type => $file_hash->{ContentType},
215                                                     data => $file_hash->{LargeContent},
216                                                     hash => md5_hex($file_hash->{LargeContent}),
217                                                 } );
218     push @results, loc("Unable to set UserLogo: [_1]", $msg) unless $id;
219
220     $imgdata = $file_hash->{LargeContent};
221 }
222 elsif ($ARGS{'reset_logo'}) {
223     RT->System->DeleteAttribute('UserLogo');
224 }
225 else {
226     if (my $attr = RT->System->FirstAttribute('UserLogo')) {
227         my $content = $attr->Content;
228         if (ref($content) eq 'HASH') {
229             $imgdata = $content->{data};
230         }
231         else {
232             RT->System->DeleteAttribute('UserLogo');
233         }
234     }
235 }
236
237 if ($user_css) {
238     if ($ARGS{'reset_css'}) {
239         RT->System->DeleteAttribute('UserCSS');
240         undef $user_css;
241     }
242     else {
243         my ($id, $msg) = RT->System->SetAttribute( Name => "UserCSS",
244                                                     Description => "User-provided css",
245                                                     Content => $user_css );
246         push @results, loc("Unable to set UserCSS: [_1]", $msg) unless $id;
247     }
248 }
249
250 if (!$user_css) {
251     my $attr = RT->System->FirstAttribute('UserCSS');
252     $user_css = $attr ? $attr->Content : join(
253         "\n\n" => map {
254             join "\n" => "/* ". $_->[0] ." */",
255                          map { "$_ {}" } @{$_->[1]}
256         } @sections
257     );
258 }
259
260 # XXX: move this to some other modules
261
262 use List::MoreUtils qw(uniq);
263
264 my $has_color_analyzer = eval { require Convert::Color; 1 };
265 my $colors;
266 my %gd_can;
267 my $valid_image_types;
268
269 if (not RT->Config->Get('DisableGD') and $has_color_analyzer) {
270     require GD;
271
272     # Always find out what GD can read...
273     for my $type (qw(Png Jpeg Gif)) {
274         $gd_can{$type}++ if GD::Image->can("newFrom${type}Data");
275     }
276     $valid_image_types = join(", ", map { uc } sort { lc $a cmp lc $b } keys %gd_can);
277
278     # ...but only analyze the image if we have data
279     if ($imgdata) {
280         if ( my $img = GD::Image->new($imgdata) ) {
281             $colors = analyze_img($img);
282         }
283         else {
284             # This has to be one damn long line because the loc() needs to be
285             # source parsed correctly.
286             push @results, loc("Automatically suggested theme colors aren't available for your image. This might be because you uploaded an image type that your installed version of GD doesn't support. Supported types are: [_1]. You can recompile libgd and GD.pm to include support for other image types.", $valid_image_types);
287         }
288     }
289 }
290
291 sub analyze_img {
292     my $img = shift;
293     my $color;
294
295     for my $i (0..$img->width-1) {
296         for my $j (0..$img->height-1) {
297             my @color = $img->rgb( $img->getPixel($i,$j) );
298             my $hsl = Convert::Color->new('rgb:'.join(',',map { $_ / 255 } @color))->convert_to('hsl');
299             my $c = join(',',@color);
300             next if $hsl->lightness < 0.1;
301             $color->{$c} ||= { h => $hsl->hue, s => $hsl->saturation, l => $hsl->lightness, cnt => 0, c => $c};
302             $color->{$c}->{cnt}++;
303         }
304     }
305
306     for (values %$color) {
307         $_->{rank} = $_->{s} * $_->{cnt};
308     }
309     my @top5 = grep { defined and $_->{'l'} and $_->{'c'} }
310                     (sort { $b->{rank} <=> $a->{rank} } values %$color)[0..5];
311     if ((scalar uniq map {$_->{rank}} @top5) == 1) {
312         warn "bad";
313     }
314     return \@top5;
315 }
316 </%INIT>
317 <%ARGS>
318 $user_css => ''
319 </%ARGS>