Master to 4.2.8
[usit-rt.git] / 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')%>/static/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 ($valid_image_types) {
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="<&|/l&>Reset to default RT Logo</&>" type="submit" />
71     <input type="submit" value="<&|/l&>Upload</&>" />
72   </form>
73 </div>
74
75 <div id="customize-theme">
76   <h2><&|/l&>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">
84         <&|/l&>Select a color for the section</&>:
85         <div id="logo-picker-hint" style="display: none;">
86           <&|/l&>You can also click on the logo above to get colors!</&>
87         </div>
88       </div>
89 % if ($colors) {
90 <div class="primary-colors">
91 %   for (@$colors) {
92 %     my $fg = $_->{l} >= $text_threshold ? 'black' : 'white';
93 <button type="button" class="color-template"
94         style="background-color: rgb(<% $_->{c} %>); color: <% $fg %>;">
95   <&|/l&>Text</&>
96 </button>
97 %   }
98 </div>
99 % }
100       <div id="color-picker"></div>
101       <canvas id="logo-color-picker" title="<&|/l&>Click to choose a color</&>"></canvas>
102     </li>
103   </ol>
104 </div>
105 </div>
106
107 <div id="custom-css">
108   <h2><&|/l&>Custom CSS (Advanced)</&></h2>
109
110   <form method="POST">
111     <textarea rows=20 id="user_css" name="user_css" wrap="off"><% $user_css %></textarea><br />
112     <input id="try" type="button" class="button" value="<&|/l&>Try</&>" />
113     <input id="reset" type="reset" value="<&|/l&>Reset</&>" type="submit" />
114     <input name="reset_css" value="<&|/l&>Reset to default RT Theme</&>" type="submit" />
115     <input value="<&|/l&>Save</&>" type="submit" />
116   </form>
117 </div>
118
119 <%ONCE>
120 my @sections = (
121     ['Page'         => ['body', 'div#body']],
122     ['Menu bar'     => ['div#quickbar', '#main-navigation #app-nav.sf-shadow > li, #main-navigation #app-nav.sf-shadow > li > a, #prefs-menu > li, #prefs-menu > li > a, #logo .rtname']],
123     ['Title bar'    => ['div#header']],
124     ['Page title'   => ['div#header h1']],
125     ['Page content' => ['div#body']],
126     ['Buttons'      => ['input[type="reset"], input[type="submit"], input[class="button"]']],
127     ['Button hover' => ['input[type="reset"]:hover, input[type="submit"]:hover, input[class="button"]:hover']],
128 );
129 </%ONCE>
130 <script type="text/javascript">
131 var section_css_mapping = <% JSON(\@sections) |n%>;
132
133 jQuery(function($) {
134
135     jQuery.each(section_css_mapping, function(i,v){
136         $('select#section').append($("<option/>")
137                            .attr('value', v[0])
138                            .text(v[0]));
139     });
140
141     function update_sitecss(text) {
142         if (!text)
143             text = $('#user_css').val();
144
145         // IE 8 doesn't let us update the innerHTML of <style> tags (with jQuery.text())
146         // see: http://stackoverflow.com/questions/2692770/style-style-textcss-appendtohead-does-not-work-in-ie/2692861#2692861
147         $("style#sitecss").remove();
148         $("<style id='sitecss' type='text/css' media='all'>" + text + "</style>").appendTo('head');
149     }
150
151     update_sitecss();
152     $('#try').click(function() {
153         update_sitecss();
154     });
155
156     $('#reset').click(function() {
157         setTimeout(function() {
158             update_sitecss();
159         }, 1000);
160     });
161
162     function change_color(bg, fg) {
163       var section = $('select#section').val();
164
165       var applying = jQuery.grep(section_css_mapping, function(a){ return a[0] == section })[0][1];
166       var css = $('#user_css').val();
167       if (applying) {
168           var specials = new RegExp("([.*+?|()\\[\\]{}\\\\])", "g");
169           for (var name in applying) {
170               var selector = (applying[name]).replace(specials, "\\$1");
171               var rule = new RegExp('^'+selector+'\\s*\{.*?\}', "m");
172               var newcss = "background: " + bg;
173
174               /* Don't set the text color on <body> as it affects too much */
175               if (applying[name] != "body")
176                   newcss += "; color: " + fg;
177
178               /* Kill the border on the quickbar if we're styling it */
179               if (applying[name].match(/quickbar/))
180                   newcss += "; border: none;"
181
182               /* Page title's text color is the selected color */
183               if (applying[name].match(/h1/))
184                   newcss = "color: " + bg;
185
186               /* Nav doesn't need a background, but it wants text color */
187               if (applying[name].match(/#main-navigation/))
188                   newcss = "color: " + fg;
189
190               css = css.replace(rule, applying[name]+" { "+newcss+" }");
191           }
192       }
193       $('#user_css').val(css);
194       update_sitecss(css);
195     }
196
197     $('#color-picker').farbtastic(function(color){ change_color(color, this.hsl[2] > <% $text_threshold %> ? '#000' : '#fff') });
198
199     $('button.color-template').click(function() {
200       change_color($(this).css('background-color'), $(this).css('color'));
201   });
202
203     // Setup the canvas color picker
204     $("#logo-theme-editor img").load(function() {
205         var logo      = $(this);
206         var canvas    = $("#logo-color-picker");
207         var el_canvas = canvas.get(0);
208
209         if (!el_canvas.getContext) return;
210
211         var context      = el_canvas.getContext("2d");
212         el_canvas.width  = logo.width();
213         el_canvas.height = logo.height();
214         context.drawImage(logo.get(0), 0, 0);
215
216         logo.hide().after(canvas);
217         canvas.show().click(function(ev) {
218             ev.preventDefault();
219             var R = 0,
220                 G = 1,
221                 B = 2,
222                 A = 3;
223             var pixel = this.getContext("2d").getImageData(ev.offsetX, ev.offsetY, 1, 1).data;
224             // Farbtastic expects values in the range of 0..1
225             var rgba  = $.makeArray(pixel).map(function(v,i) { return v / 255 });
226             var wheel = $.farbtastic("#color-picker");
227             wheel.setHSL( wheel.RGBToHSL( rgba.slice(R,A) ) );
228             // XXX TODO factor in the alpha channel too
229         });
230         $('#logo-picker-hint').show();
231     });
232 });
233 </script>
234 <%INIT>
235 unless ($session{'CurrentUser'}->HasRight( Object=> RT->System, Right => 'SuperUser')) {
236     Abort(loc('This feature is only available to system administrators.'));
237 }
238
239 use Digest::MD5 'md5_hex';
240
241 my $text_threshold = 0.6;
242 my @results;
243 my $imgdata;
244
245 my $colors;
246 my $valid_image_types;
247 my $analyze_img = sub {
248     return undef if RT->Config->Get('DisableGD');
249     return undef unless Convert::Color->require;
250
251     require GD;
252
253     # Always find out what GD can read...
254     my %gd_can;
255     for my $type (qw(Png Jpeg Gif)) {
256         $gd_can{$type}++ if GD::Image->can("newFrom${type}Data");
257     }
258     $valid_image_types = join(", ", map { uc } sort { lc $a cmp lc $b } keys %gd_can);
259
260     my $imgdata = shift;
261     return undef unless $imgdata;
262
263     # ...but only analyze the image if we have data
264     my $img = GD::Image->new($imgdata);
265     unless ($img) {
266         # This has to be one damn long line because the loc() needs to be
267         # source parsed correctly.
268         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);
269         return undef;
270     }
271
272     my %colors;
273
274     my @wsamples;
275     my @hsamples;
276     if ($img->width > 200) {
277         @wsamples = map { int($img->width*($_/200)) } (0..199);
278     } else {
279         @wsamples = ( 0 .. $img->width - 1 );
280     }
281     if ($img->height > 200) {
282         @hsamples = map { int($img->height*($_/200)) } (0..199);
283     } else {
284         @hsamples = ( 0 .. $img->height - 1 );
285     }
286     for my $i (@wsamples) {
287         for my $j (@hsamples) {
288             my @color = $img->rgb( $img->getPixel($i,$j) );
289             my $hsl = Convert::Color->new('rgb:'.join(',',map { $_ / 255 } @color))->convert_to('hsl');
290             my $c = join(',',@color);
291             next if $hsl->lightness < 0.1;
292             $colors{$c} ||= { h => $hsl->hue, s => $hsl->saturation, l => $hsl->lightness, cnt => 0, c => $c};
293             $colors{$c}->{cnt}++;
294         }
295     }
296
297     use List::MoreUtils qw(uniq);
298     for (values %colors) {
299         $_->{rank} = $_->{s} * $_->{cnt};
300     }
301     my @top5 = grep { defined and $_->{'l'} and $_->{'c'} }
302                     (sort { $b->{rank} <=> $a->{rank} } values %colors)[0..5];
303     return \@top5;
304 };
305
306 if (my $file_hash = _UploadedFile( 'logo-upload' )) {
307     $colors = $analyze_img->($file_hash->{LargeContent});
308
309     my $my_system = RT::System->new( $session{CurrentUser} );
310     my ( $id, $msg ) = $my_system->SetAttribute(
311         Name        => "UserLogo",
312         Description => "User-provided logo",
313         Content     => {
314             type => $file_hash->{ContentType},
315             data => $file_hash->{LargeContent},
316             hash => md5_hex($file_hash->{LargeContent}),
317             colors => $colors,
318         },
319     );
320
321     push @results, loc("Unable to set UserLogo: [_1]", $msg) unless $id;
322
323     $imgdata = $file_hash->{LargeContent};
324 }
325 elsif ($ARGS{'reset_logo'}) {
326     RT->System->DeleteAttribute('UserLogo');
327 }
328 else {
329     if (my $attr = RT->System->FirstAttribute('UserLogo')) {
330         my $content = $attr->Content;
331         if (ref($content) eq 'HASH') {
332             $imgdata = $content->{data};
333             $colors = $content->{colors};
334             unless ($colors) {
335                 # No colors cached; attempt to generate them
336                 $colors = $content->{colors} = $analyze_img->($content->{data});
337                 if ($content->{colors}) {
338                     # Found colors; update the attribute
339                     RT->System->SetAttribute(
340                         Name => "UserLogo",
341                         Description => "User-provided logo",
342                         Content => $content,
343                     );
344                 }
345             }
346         }
347         else {
348             RT->System->DeleteAttribute('UserLogo');
349         }
350     }
351 }
352
353 if ($user_css) {
354     if ($ARGS{'reset_css'}) {
355         RT->System->DeleteAttribute('UserCSS');
356         undef $user_css;
357     }
358     else {
359         my ($id, $msg) = RT->System->SetAttribute( Name => "UserCSS",
360                                                     Description => "User-provided css",
361                                                     Content => $user_css );
362         push @results, loc("Unable to set UserCSS: [_1]", $msg) unless $id;
363     }
364 }
365
366 if (!$user_css) {
367     my $attr = RT->System->FirstAttribute('UserCSS');
368     $user_css = $attr ? $attr->Content : join(
369         "\n\n" => map {
370             join "\n" => "/* ". $_->[0] ." */",
371                          map { "$_ {}" } @{$_->[1]}
372         } @sections
373     );
374 }
375 </%INIT>
376 <%ARGS>
377 $user_css => ''
378 </%ARGS>