Master to 4.2.8
[usit-rt.git] / share / html / Admin / Tools / Theme.html
CommitLineData
84fb5b46
MKG
1%# BEGIN BPS TAGGED BLOCK {{{
2%#
3%# COPYRIGHT:
4%#
320f0092 5%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
84fb5b46
MKG
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
af59614d 54<script type="text/javascript" src="<%RT->Config->Get('WebPath')%>/static/js/farbtastic.js"></script>
84fb5b46
MKG
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">
c33a4027 64% if ($valid_image_types) {
84fb5b46
MKG
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>
320f0092
MKG
70 <input name="reset_logo" value="<&|/l&>Reset to default RT Logo</&>" type="submit" />
71 <input type="submit" value="<&|/l&>Upload</&>" />
84fb5b46
MKG
72 </form>
73</div>
74
75<div id="customize-theme">
320f0092 76 <h2><&|/l&>Customize the RT theme</&></h2>
84fb5b46
MKG
77 <ol>
78 <li>
79 <label for="section"><&|/l&>Select a section</&>:</label>
80 <select id="section"></select>
81 </li>
82 <li>
af59614d
MKG
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>
84fb5b46
MKG
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>
af59614d 101 <canvas id="logo-color-picker" title="<&|/l&>Click to choose a color</&>"></canvas>
84fb5b46
MKG
102 </li>
103 </ol>
104</div>
105</div>
106
107<div id="custom-css">
320f0092 108 <h2><&|/l&>Custom CSS (Advanced)</&></h2>
af59614d 109
84fb5b46
MKG
110 <form method="POST">
111 <textarea rows=20 id="user_css" name="user_css" wrap="off"><% $user_css %></textarea><br />
320f0092
MKG
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" />
84fb5b46
MKG
116 </form>
117</div>
118
119<%ONCE>
120my @sections = (
af59614d
MKG
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']],
84fb5b46
MKG
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">
131var section_css_mapping = <% JSON(\@sections) |n%>;
132
133jQuery(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
320f0092
MKG
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();
84fb5b46 152 $('#try').click(function() {
320f0092 153 update_sitecss();
84fb5b46
MKG
154 });
155
156 $('#reset').click(function() {
157 setTimeout(function() {
320f0092 158 update_sitecss();
84fb5b46
MKG
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 */
af59614d 183 if (applying[name].match(/h1/))
84fb5b46
MKG
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);
320f0092 194 update_sitecss(css);
84fb5b46
MKG
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
af59614d
MKG
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 });
84fb5b46
MKG
232});
233</script>
234<%INIT>
235unless ($session{'CurrentUser'}->HasRight( Object=> RT->System, Right => 'SuperUser')) {
236 Abort(loc('This feature is only available to system administrators.'));
237}
238
239use Digest::MD5 'md5_hex';
240
241my $text_threshold = 0.6;
242my @results;
243my $imgdata;
244
c33a4027
MKG
245my $colors;
246my $valid_image_types;
247my $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
84fb5b46 306if (my $file_hash = _UploadedFile( 'logo-upload' )) {
c33a4027
MKG
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
84fb5b46
MKG
321 push @results, loc("Unable to set UserLogo: [_1]", $msg) unless $id;
322
323 $imgdata = $file_hash->{LargeContent};
324}
325elsif ($ARGS{'reset_logo'}) {
326 RT->System->DeleteAttribute('UserLogo');
327}
328else {
329 if (my $attr = RT->System->FirstAttribute('UserLogo')) {
330 my $content = $attr->Content;
331 if (ref($content) eq 'HASH') {
332 $imgdata = $content->{data};
c33a4027
MKG
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 }
84fb5b46
MKG
346 }
347 else {
348 RT->System->DeleteAttribute('UserLogo');
349 }
350 }
351}
352
353if ($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
366if (!$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}
84fb5b46
MKG
375</%INIT>
376<%ARGS>
377$user_css => ''
378</%ARGS>