From be06bb05b8e580650bfb0564960ec621f1351a5a Mon Sep 17 00:00:00 2001 From: Tim Taubert Date: Tue, 3 Jul 2012 08:22:10 +0200 Subject: [PATCH 1/2] Bug 769634 - imgITools should provide cropping functionality; r=bbondy --- image/public/imgITools.idl | 33 +++- image/src/imgTools.cpp | 199 +++++++++++++------- image/src/imgTools.h | 10 + image/test/unit/image2jpg16x16cropped.jpg | Bin 0 -> 879 bytes image/test/unit/image2jpg16x16cropped2.jpg | Bin 0 -> 878 bytes image/test/unit/image2jpg16x32cropped3.jpg | Bin 0 -> 1127 bytes image/test/unit/image2jpg16x32scaled.jpg | Bin 0 -> 1219 bytes image/test/unit/image2jpg32x16cropped4.jpg | Bin 0 -> 1135 bytes image/test/unit/image2jpg32x16scaled.jpg | Bin 0 -> 1227 bytes image/test/unit/image2jpg32x32.jpg | Bin 0 -> 1634 bytes image/test/unit/test_imgtools.js | 208 +++++++++++++++++++++ 11 files changed, 382 insertions(+), 68 deletions(-) create mode 100644 image/test/unit/image2jpg16x16cropped.jpg create mode 100644 image/test/unit/image2jpg16x16cropped2.jpg create mode 100644 image/test/unit/image2jpg16x32cropped3.jpg create mode 100644 image/test/unit/image2jpg16x32scaled.jpg create mode 100644 image/test/unit/image2jpg32x16cropped4.jpg create mode 100644 image/test/unit/image2jpg32x16scaled.jpg create mode 100644 image/test/unit/image2jpg32x32.jpg diff --git a/image/public/imgITools.idl b/image/public/imgITools.idl index cd67925a7f2..147aac073c2 100644 --- a/image/public/imgITools.idl +++ b/image/public/imgITools.idl @@ -9,7 +9,7 @@ interface nsIInputStream; interface imgIContainer; -[scriptable, uuid(1f19a2ce-cf5c-4a6b-8ba7-63785b45053f)] +[scriptable, uuid(8e16f39e-7012-46bd-aa22-2a7a3265608f)] interface imgITools : nsISupports { /** @@ -60,7 +60,8 @@ interface imgITools : nsISupports * @param aMimeType * Type of encoded image desired (eg "image/png"). * @param aWidth, aHeight - * The size (in pixels) desired for the resulting image. + * The size (in pixels) desired for the resulting image. Specify 0 to + * use the given image's width or height. Values must be >= 0. * @param outputOptions * Encoder-specific output options. */ @@ -69,4 +70,32 @@ interface imgITools : nsISupports in long aWidth, in long aHeight, [optional] in AString outputOptions); + + /** + * encodeCroppedImage + * Caller provides an image container, and the mime type it should be + * encoded to. We return an input stream for the encoded image data. + * The encoded image is cropped to the specified dimensions. + * + * The given offset and size must not exceed the image bounds. + * + * @param aContainer + * An image container. + * @param aMimeType + * Type of encoded image desired (eg "image/png"). + * @param aOffsetX, aOffsetY + * The crop offset (in pixels). Values must be >= 0. + * @param aWidth, aHeight + * The size (in pixels) desired for the resulting image. Specify 0 to + * use the given image's width or height. Values must be >= 0. + * @param outputOptions + * Encoder-specific output options. + */ + nsIInputStream encodeCroppedImage(in imgIContainer aContainer, + in ACString aMimeType, + in long aOffsetX, + in long aOffsetY, + in long aWidth, + in long aHeight, + [optional] in AString outputOptions); }; diff --git a/image/src/imgTools.cpp b/image/src/imgTools.cpp index 9b17279bdcc..33bb59f7ae4 100644 --- a/image/src/imgTools.cpp +++ b/image/src/imgTools.cpp @@ -102,12 +102,14 @@ NS_IMETHODIMP imgTools::EncodeImage(imgIContainer *aContainer, const nsAString& aOutputOptions, nsIInputStream **aStream) { - return EncodeScaledImage(aContainer, - aMimeType, - 0, - 0, - aOutputOptions, - aStream); + nsresult rv; + + // Use frame 0 from the image container. + nsRefPtr frame; + rv = GetFirstImageFrame(aContainer, getter_AddRefs(frame)); + NS_ENSURE_SUCCESS(rv, rv); + + return EncodeImageData(frame, aMimeType, aOutputOptions, aStream); } NS_IMETHODIMP imgTools::EncodeScaledImage(imgIContainer *aContainer, @@ -117,20 +119,111 @@ NS_IMETHODIMP imgTools::EncodeScaledImage(imgIContainer *aContainer, const nsAString& aOutputOptions, nsIInputStream **aStream) { - nsresult rv; - bool doScaling = true; - PRUint8 *bitmapData; - PRUint32 bitmapDataLength, strideSize; + NS_ENSURE_ARG(aScaledWidth >= 0 && aScaledHeight >= 0); // If no scaled size is specified, we'll just encode the image at its // original size (no scaling). if (aScaledWidth == 0 && aScaledHeight == 0) { - doScaling = false; - } else { - NS_ENSURE_ARG(aScaledWidth > 0); - NS_ENSURE_ARG(aScaledHeight > 0); + return EncodeImage(aContainer, aMimeType, aOutputOptions, aStream); } + // Use frame 0 from the image container. + nsRefPtr frame; + nsresult rv = GetFirstImageFrame(aContainer, getter_AddRefs(frame)); + NS_ENSURE_SUCCESS(rv, rv); + + PRInt32 frameWidth = frame->Width(), frameHeight = frame->Height(); + + // If the given width or height is zero we'll replace it with the image's + // original dimensions. + if (aScaledWidth == 0) { + aScaledWidth = frameWidth; + } else if (aScaledHeight == 0) { + aScaledHeight = frameHeight; + } + + // Create a temporary image surface + nsRefPtr dest = new gfxImageSurface(gfxIntSize(aScaledWidth, aScaledHeight), + gfxASurface::ImageFormatARGB32); + gfxContext ctx(dest); + + // Set scaling + gfxFloat sw = (double) aScaledWidth / frameWidth; + gfxFloat sh = (double) aScaledHeight / frameHeight; + ctx.Scale(sw, sh); + + // Paint a scaled image + ctx.SetOperator(gfxContext::OPERATOR_SOURCE); + ctx.SetSource(frame); + ctx.Paint(); + + return EncodeImageData(dest, aMimeType, aOutputOptions, aStream); +} + +NS_IMETHODIMP imgTools::EncodeCroppedImage(imgIContainer *aContainer, + const nsACString& aMimeType, + PRInt32 aOffsetX, + PRInt32 aOffsetY, + PRInt32 aWidth, + PRInt32 aHeight, + const nsAString& aOutputOptions, + nsIInputStream **aStream) +{ + NS_ENSURE_ARG(aOffsetX >= 0 && aOffsetY >= 0 && aWidth >= 0 && aHeight >= 0); + + // Offsets must be zero when no width and height are given or else we're out + // of bounds. + NS_ENSURE_ARG(aWidth + aHeight > 0 || aOffsetX + aOffsetY == 0); + + // If no size is specified then we'll preserve the image's original dimensions + // and don't need to crop. + if (aWidth == 0 && aHeight == 0) { + return EncodeImage(aContainer, aMimeType, aOutputOptions, aStream); + } + + // Use frame 0 from the image container. + nsRefPtr frame; + nsresult rv = GetFirstImageFrame(aContainer, getter_AddRefs(frame)); + NS_ENSURE_SUCCESS(rv, rv); + + PRInt32 frameWidth = frame->Width(), frameHeight = frame->Height(); + + // If the given width or height is zero we'll replace it with the image's + // original dimensions. + if (aWidth == 0) { + aWidth = frameWidth; + } else if (aHeight == 0) { + aHeight = frameHeight; + } + + // Check that the given crop rectangle is within image bounds. + NS_ENSURE_ARG(frameWidth >= aOffsetX + aWidth && + frameHeight >= aOffsetY + aHeight); + + // Create a temporary image surface + nsRefPtr dest = new gfxImageSurface(gfxIntSize(aWidth, aHeight), + gfxASurface::ImageFormatARGB32); + gfxContext ctx(dest); + + // Set translate + ctx.Translate(gfxPoint(-aOffsetX, -aOffsetY)); + + // Paint a scaled image + ctx.SetOperator(gfxContext::OPERATOR_SOURCE); + ctx.SetSource(frame); + ctx.Paint(); + + return EncodeImageData(dest, aMimeType, aOutputOptions, aStream); +} + +NS_IMETHODIMP imgTools::EncodeImageData(gfxImageSurface *aSurface, + const nsACString& aMimeType, + const nsAString& aOutputOptions, + nsIInputStream **aStream) +{ + PRUint8 *bitmapData; + PRUint32 bitmapDataLength, strideSize; + // Get an image encoder for the media type nsCAutoString encoderCID( NS_LITERAL_CSTRING("@mozilla.org/image/encoder;2?type=") + aMimeType); @@ -139,65 +232,39 @@ NS_IMETHODIMP imgTools::EncodeScaledImage(imgIContainer *aContainer, if (!encoder) return NS_IMAGELIB_ERROR_NO_ENCODER; - // Use frame 0 from the image container. - nsRefPtr frame; - rv = aContainer->CopyFrame(imgIContainer::FRAME_CURRENT, true, - getter_AddRefs(frame)); - NS_ENSURE_SUCCESS(rv, rv); - if (!frame) - return NS_ERROR_NOT_AVAILABLE; - - PRInt32 w = frame->Width(), h = frame->Height(); - if (!w || !h) + bitmapData = aSurface->Data(); + if (!bitmapData) return NS_ERROR_FAILURE; - nsRefPtr dest; + strideSize = aSurface->Stride(); - if (!doScaling) { - // If we're not scaling the image, use the actual width/height. - aScaledWidth = w; - aScaledHeight = h; - - bitmapData = frame->Data(); - if (!bitmapData) - return NS_ERROR_FAILURE; - - strideSize = frame->Stride(); - bitmapDataLength = aScaledHeight * strideSize; - - } else { - // Prepare to draw a scaled version of the image to a temporary surface... - - // Create a temporary image surface - dest = new gfxImageSurface(gfxIntSize(aScaledWidth, aScaledHeight), - gfxASurface::ImageFormatARGB32); - gfxContext ctx(dest); - - // Set scaling - gfxFloat sw = (double) aScaledWidth / w; - gfxFloat sh = (double) aScaledHeight / h; - ctx.Scale(sw, sh); - - // Paint a scaled image - ctx.SetOperator(gfxContext::OPERATOR_SOURCE); - ctx.SetSource(frame); - ctx.Paint(); - - bitmapData = dest->Data(); - strideSize = dest->Stride(); - bitmapDataLength = aScaledHeight * strideSize; - } + PRInt32 width = aSurface->Width(), height = aSurface->Height(); + bitmapDataLength = height * strideSize; // Encode the bitmap - rv = encoder->InitFromData(bitmapData, - bitmapDataLength, - aScaledWidth, - aScaledHeight, - strideSize, - imgIEncoder::INPUT_FORMAT_HOSTARGB, - aOutputOptions); + nsresult rv = encoder->InitFromData(bitmapData, + bitmapDataLength, + width, + height, + strideSize, + imgIEncoder::INPUT_FORMAT_HOSTARGB, + aOutputOptions); NS_ENSURE_SUCCESS(rv, rv); return CallQueryInterface(encoder, aStream); } + +NS_IMETHODIMP imgTools::GetFirstImageFrame(imgIContainer *aContainer, + gfxImageSurface **aSurface) +{ + nsRefPtr frame; + nsresult rv = aContainer->CopyFrame(imgIContainer::FRAME_CURRENT, true, + getter_AddRefs(frame)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(frame, NS_ERROR_NOT_AVAILABLE); + NS_ENSURE_TRUE(frame->Width() && frame->Height(), NS_ERROR_FAILURE); + + frame.forget(aSurface); + return NS_OK; +} diff --git a/image/src/imgTools.h b/image/src/imgTools.h index c06d59c7fa8..02ccbab56c8 100644 --- a/image/src/imgTools.h +++ b/image/src/imgTools.h @@ -5,6 +5,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "imgITools.h" +#include "gfxContext.h" #define NS_IMGTOOLS_CID \ { /* fd9a9e8a-a77b-496a-b7bb-263df9715149 */ \ @@ -22,4 +23,13 @@ public: imgTools(); virtual ~imgTools(); + +private: + NS_IMETHODIMP EncodeImageData(gfxImageSurface *aSurface, + const nsACString& aMimeType, + const nsAString& aOutputOptions, + nsIInputStream **aStream); + + NS_IMETHODIMP GetFirstImageFrame(imgIContainer *aContainer, + gfxImageSurface **aSurface); }; diff --git a/image/test/unit/image2jpg16x16cropped.jpg b/image/test/unit/image2jpg16x16cropped.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fca22cb30a8e5538d8029b1681cf557ff2556629 GIT binary patch literal 879 zcmex=>ukC3pCfH06P05XITq?4J21E^7eo0A(TN+S4wfI*Oh zL4ZMknNg5|Nsy6Qkn#T!26>=6SsB5A0SXwIm|0la*f}`4xPc0`3NSDM?Pg+OW@Tkz z0jjPA$}_MCvI;30I#U-U>$dGXcJ4ZK_{h;?$4{I*b?NeztJkjIxOwa0qsLF4 zK70P+<*SdMK7aZ8?fZ|Pzd-(CWMGDP3rIpdhUPC|ATTnq0E3hrWKS#c@69{;yl(wme1fGL-^|! z0}nGJF!GoM8SEK;uYMfS@qT zne=1x1^+V~a{MSAe=Ty7Y4-HinGuYeWRC6LduX}c&-KsbRE}La_N;i~-^jxf5)X@h z1ZO>;moZQAqTbV4zYpJ2F^WAAbaURNuQe+smK*9ddKQ%@Bu?Rb^2L5pMegKh#_OJ+ cJuSB{{rNoYG|5^StA#=HO|HJZbo~EK0Nf=>>ukC3pCfH06P05XITq?4J21E^7eo0A(TN+S4wfI*Oh zL4ZMknNg5|Nsy6Qkn#T!26>=6SsB5A0SXwIm|0la*f}`4xPc0`3NSDM?Pg+OW@Tkz z0jjPA$}_MCvI;30I#U-U>$dGXcJ4ZK_{h;?$4{I*b?NeztJkjIxOwa0qsLF4 zK70P+<*SdMK7aZ8?fZ|Pzd-(CWMGDP3rIpdhUPC|ATTnq0E3hrWKS#c@69{;yl(wme1fGL-^|! z0}nGJF!GoM8SEK;8XuYeVOHIx{X%baO8Qwm7i3nYq}_Y;Fr_kJL0P`xlKLqgr?)yW z_w?ONj!N3m|2OjVoXu9Rmb_iM|AE|EJ&C^bo@_?F5Y5ex*)ovurhzy zi;u?z4&AW{P$@U+|0bsK{J4At*S~}lmmaOt{PpG9+xek0uKYW5@HodljdwLaKkWDW zc>Rd<3FX-jk8Ld4-Dv2X@lpA=NskEQyNK|eJr8Z}%l)0o_V(`Xf9WqO_f=1Ox}ok= z+4P0q(+|yWyEVW4W{u~g{|uXYqu=~zNZq_?>$Cg5+a7<>ukC3pCfH06P05XITq?4J21E^7eo0A(TN+S4wfI*Oh zL4iSlnNg5|Nsy6Qkn#T!26>=6SsB5A0SXwIm|0la*f}`4xPc0`3NSDM?Pg+OW@Tkz z0jjPA$}_MCvI;30I#U-U>$dGXcJ4ZK_{h;?$4{I*b?NeztJkjIxOwa0qsLF4 zK70P+<*SdMK7aZ8?fZ|Pzd-(CWMGDP3rIpdhUPC|ATTnq0E3hrWKS#c@69{;yl(wme1fGL-^|! z0}nGJF!GoM8SEK;uYMfS@qT zne=1x1^+V~a{MSAe=Ty7Y4-HinGuYeWRC6LduX}c&-KsbRE}La_N;i~-^jxf5)X@h z1ZO>;moZQAqTbV4zYpJ2F^WAAbaURNuQe+smK*9ddKQ%@Bu?Rb^2L5pMegKh#_OJ+ zJuSB{{rNoYG|5^StA#=HO|HJZbnN-j^Sox=ABB(Y=lz;ilD>E6uEzezTzxsuiBsn9 zVMud+FCBj5V;I-6$eC6bqHOywSHw%oK9o~EEB;$eYQJ*rr9bD@dn}4I&ZvI96RMZC*Z;!$Ta&+K zbU&WfpS!jqJ;+!3*etE>-%9^8D5m;6KQX1R?A;T&vUx|u*uTWw*H}6$c+0ib$)DGW zKI)odA9pF{%F|u;_FLZyf1C0ndga;W70(Zde`=jD-%8tUme|*frw-wnueW@#Vp!bx e^O}CkT+KMG^E-b}6Sq6DIm#>K`TEf0|2F|>ukC3pCfH06P05XITq?4J21E^7eo0A(TN+S4wfI*Oh zL4iSlnNg5|Nsy6Qkn#T!26>=6SsB5A0SXwIm|0la*f}`4xPc0`3NSDM?Pg+OW@Tkz z0jjPA$}_MCvI;30I#U-U>$dGXcJ4ZK_{h;?$4{I*b?NeztJkjIxOwa0qsLF4 zK70P+<*SdMK7aZ8?fZ|Pzd-(CWMGDP3rIpdhUPC|ATTnq0E3hrWKS#c@69{;yl(wme1fGL-^|! z0}nGJF!GoM8SEK;U4L}`*j@jKkFP7(y(foVGVxV=ny_#0)`<^logSaLb@8}Kiq(?D zqTd`UbA&|w-fo(6T6eX&ThZQhby2x%;W?joG&9e9n6c_Z?@Eo8mQ}k|qJA)b%=%|9 z@j8z2BY)?!D>)*!{g>zXUXP+< zt1iXLOn3b7^fS+{&?)OR_J&U{es*8>N7u2}kNB2W+bz@0*WW6=dv!=(q4eg>8&xYT z1LAcUo}{D*PyDIR6Lj?1oWQplyQ|-BznN8c^C&;>^n#vgN;an_Y3jZAuAe#ggZiVE zf4V=eu8YbtynT4(k6gXEHIsPyGldo_O*^i*@VY2{@5Qhja%nX*zdr#Z2P6> zH~*+^T`l7q`Yn}h-?xe9YXUF(x0}8#4$YN&9kTPo+q2JR>+ap1R2Gq*n&uzm^zL{N zzpQJWchj+CBRhq;GA2KzCwt!Bcl^sabM~flKkBq4j{?IsV&Qc3Mhk`dclxtl;c@?N4Xs zMQz$+TT^OL(z%TgVX>(>ukC3pCfH06P05XITq?4J21E^7eo0A(TN+S4wfI*Oh zL4ZMlnNg5|Nsy6Qkn#T!26>=6SsB5A0SXwIm|0la*f}`4xPc0`3NSDM?Pg+OW@Tkz z0jjPA$}_MCvI;30I#U-U>$dGXcJ4ZK_{h;?$4{I*b?NeztJkjIxOwa0qsLF4 zK70P+<*SdMK7aZ8?fZ|Pzd-(CWMGDP3rIpdhUPC|ATTnq0E3hrWKS#c@69{;yl(wme1fGL-^|! z0}nGJF!GoM8SEK;uYMfS@qT zi8%Eidmq=ge!g9?{&2idskbfbyU?`BM~Y9cjP#V|J-x@r=ak|6nJPUzMK_ku51J;- z_B!#!=d&eIf`@l@9eK33H)~qA-PRBCM`Oj03B3IF**jnK-{LI2wfoK+%)9l;cEL=A zsYUDdH1|A7-*M1>&J&r?_H$3Qbc;g!KBm8j*t07l^{vv+MZe}t)ntB@|Dz%=xb<)9 zOy#hh6_HaLq!rI(@4ll|{C?J-*`0=44R_9YQg{2Xgv7(5AHiAA=Vi=Oyr}nd*6+jj zRE%Oz1l^o>>1)l3iRFfRjh;p235iqqo_w)iRFOORnen>kXHU!ROMgC3J591y#%f{E ze3PqhFCFLpX#B19$`^f^=nDIzVO2#x>K-|NYo9H&=G*#)zEedZtJ}_1SiC%z{maNC zQL5@)y~)E3p=x&?F1DNcch1%w3qJ7w(cv|7{>E+nWZJ27N=zI+tA0Ozki*ycz3^D{ nb>^SyJ%v|{&RI^4(d*66+a>ukC3pCfH06P05XITq?4J21E^7eo0A(TN+S4wfI*Oh zL4ZMlnNg5|Nsy6Qkn#T!26>=6SsB5A0SXwIm|0la*f}`4xPc0`3NSDM?Pg+OW@Tkz z0jjPA$}_MCvI;30I#U-U>$dGXcJ4ZK_{h;?$4{I*b?NeztJkjIxOwa0qsLF4 zK70P+<*SdMK7aZ8?fZ|Pzd-(CWMGDP3rIpdhUPC|ATTnq0E3hrWKS#c@69{;yl(wme1fGL-^|! z0}nGJF!GoM8SEK;6+iZWEB!dSUij54_QpRByV7Ro+)t05k-Mm}ThC>Q$*NQP4HRTb zBp+OVcH^Jzyx195B`dWi#L8ZOQrA7bK&9AbuisL4zs0ZC{AXb6*Sch9lvO^fW8amp z)2|1~3BbzA?UAJ0QSoZIoa$!zPW1MZSue8TnV#pnMsWbogtKQ2|ac6CMn!+S0JG(P-Wue05}%E)Wuy{J8F z&YDk=X`L!LbB3NXQ(1!>*L{J4Tp!=^?gP6$zx-!V{vs##*hfdxoqNWh|_e z`FcWsi$KZAKc?uoYbz~tir-B9WUkJ)(l7AO7Q5}n zr;6$n<8B!*w)|eR{zJO`lDf0kMHg^ZcaX dw~OnoGxiHVFSfZiMRScF@9*yXAI1M~0suG&?CSsk literal 0 HcmV?d00001 diff --git a/image/test/unit/image2jpg32x32.jpg b/image/test/unit/image2jpg32x32.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cf9a10a37feb5b99a4516c5e4af050e99be2b8ca GIT binary patch literal 1634 zcmbV}c{tQ*9Ke5bF&AbA5tBhm3FTN_yIHJCRBIfiW2GD+>>z*Y1Aa=lwqK^ST1dEC_-Htu+7y$vt79uK<4n z0z;rMDQP$YiIOC6FaTI`HW&(l!Jtq{_BF{m0L8*^8U|)k^0qG0nj{6oFnSJrvw3m7 zBC&l|%ji5g9D&@Rq`XmO%U11e+jWe;Gugdo?>>v4EDs(!Y<0xW-of$2NvBg6T;1F~ zE_!g}}jj61n^^X}!}e^5~Jq?Gftth}P~Ik%y) z>BY-e%^jUxZ{KzI@Ot?}!-A2~vGETR!cTMa3!=p(@$$MC2!MRklEiOf|L~GL0tgJ1 zJgM}$7YH0E=@2XwrePq3GqaU;A<1hRhQSrg={d#qh|NaCS;h0@cH{;vWBwN5y4qK> ze~SCPRYZobye;vEK^rXO}fyiu|}`kXGB;Qzwk5- zq0Q{HuFO2fDYc)-@!_4h@0T3(sI`9q7@pYW6lIsdxbG%PqRo84FWJ1Kk#5_?Q-eo{ zJmp`sm#T+DBj@@kY_2ozvh@$Jj}Goh4gM_;oUBx3TB>YpnhjX#T5)J{PdCD%*8sOHTsX78sTveVU5$d9Tb z;shS$^`;B~-XXeeL`RS8Kofe5pRB4p={Bt?zO&3yp?V4lpS1{B&Z=AbX2%MOQ_6K! zjaKhx{W|_L8J{y_3^)cA3_(aAS6P`)Ub6EVV_sxxwCj&#tZ?6J8J>MfoNOq*0?sW3OA(YCOi6JKQ&3!|%%>vW(wOcf3FejYN@M^E>gU zDW;8%!Q`coAq!D-Chnn#eVx`!wl9ooqwM+6ppn-S7ci<{C7NPt>pdj`9@zaQee2K}6U z#n0ewX!79}jyWWb#=GGBc&I#K)v1N?WZ%Ty>Ac!O0-eaeIZA1ItPrFb@_=eI8%Ls8 za8JR?qm@#ZeG~8`v>vw+e8_*>8sKuCMkOs?XOE)e^>v0=?4!QC1*iDqQ?2-4V~1#C zfh)w2*H|jKeVW5YPgvzrIxQW)wABi9A27r?dfPyML|bQ1c-vIj!c0 z6j~l5us!v?$O8mcuPA2a2vx6Zw6n^85G6K$c|1!o_b4j3l$^bzbfK5B39zmEJgM~j zSTbADqkpvp#d38j+M?s%T~It8ikhFGu|20!LV#U(;XR^IqE-fTYqX+B@%jHA>} Date: Tue, 3 Jul 2012 14:40:12 +0100 Subject: [PATCH 2/2] Bug 651942 - Add a list of recently-opened files to the file menu of the Scratchpad. r=harth --- browser/app/profile/firefox.js | 5 + .../scratchpad/scratchpad-manager.jsm | 2 +- browser/devtools/scratchpad/scratchpad.js | 252 +++++++++++++- browser/devtools/scratchpad/scratchpad.xul | 6 + browser/devtools/scratchpad/test/Makefile.in | 1 + ...wser_scratchpad_bug_651942_recent_files.js | 314 ++++++++++++++++++ .../chrome/browser/devtools/scratchpad.dtd | 3 + .../browser/devtools/scratchpad.properties | 4 + 8 files changed, 577 insertions(+), 10 deletions(-) create mode 100644 browser/devtools/scratchpad/test/browser_scratchpad_bug_651942_recent_files.js diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 7134163067f..cd898b668cd 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1073,6 +1073,11 @@ pref("devtools.ruleview.enabled", true); // Enable the Scratchpad tool. pref("devtools.scratchpad.enabled", true); +// The maximum number of recently-opened files stored. +// Setting this preference to 0 will not clear any recent files, but rather hide +// the 'Open Recent'-menu. +pref("devtools.scratchpad.recentFilesMax", 10); + // Enable the Style Editor. pref("devtools.styleeditor.enabled", true); pref("devtools.styleeditor.transitions", true); diff --git a/browser/devtools/scratchpad/scratchpad-manager.jsm b/browser/devtools/scratchpad/scratchpad-manager.jsm index 2749119c86a..2fdbf962c74 100644 --- a/browser/devtools/scratchpad/scratchpad-manager.jsm +++ b/browser/devtools/scratchpad/scratchpad-manager.jsm @@ -101,7 +101,7 @@ var ScratchpadManager = { } let win = Services.ww.openWindow(null, SCRATCHPAD_WINDOW_URL, "_blank", SCRATCHPAD_WINDOW_FEATURES, params); - // Only add shutdown observer if we've opened a scratchpad window + // Only add the shutdown observer if we've opened a scratchpad window. ShutdownObserver.init(); return win; diff --git a/browser/devtools/scratchpad/scratchpad.js b/browser/devtools/scratchpad/scratchpad.js index 599ee25c855..4c47d2fc972 100644 --- a/browser/devtools/scratchpad/scratchpad.js +++ b/browser/devtools/scratchpad/scratchpad.js @@ -31,6 +31,7 @@ const SCRATCHPAD_CONTEXT_CONTENT = 1; const SCRATCHPAD_CONTEXT_BROWSER = 2; const SCRATCHPAD_L10N = "chrome://browser/locale/devtools/scratchpad.properties"; const DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled"; +const PREF_RECENT_FILES_MAX = "devtools.scratchpad.recentFilesMax"; const BUTTON_POSITION_SAVE = 0; const BUTTON_POSITION_CANCEL = 1; const BUTTON_POSITION_DONT_SAVE = 2; @@ -160,7 +161,7 @@ var Scratchpad = { * @param object aState * An object with filename and executionContext properties. */ - setState: function SP_getState(aState) + setState: function SP_setState(aState) { if (aState.filename) { this.setFilename(aState.filename); @@ -615,19 +616,203 @@ var Scratchpad = { /** * Open a file to edit in the Scratchpad. + * + * @param integer aIndex + * Optional integer: clicked menuitem in the 'Open Recent'-menu. */ - openFile: function SP_openFile() + openFile: function SP_openFile(aIndex) { - let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); - fp.init(window, this.strings.GetStringFromName("openFile.title"), - Ci.nsIFilePicker.modeOpen); - fp.defaultString = ""; - if (fp.show() != Ci.nsIFilePicker.returnCancel) { - this.setFilename(fp.file.path); - this.importFromFile(fp.file, false); + let fp; + if (!aIndex && aIndex !== 0) { + fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + fp.init(window, this.strings.GetStringFromName("openFile.title"), + Ci.nsIFilePicker.modeOpen); + fp.defaultString = ""; + } + + if (aIndex > -1 || fp.show() != Ci.nsIFilePicker.returnCancel) { + this.promptSave(function(aCloseFile, aSaved, aStatus) { + let shouldOpen = aCloseFile; + if (aSaved && !Components.isSuccessCode(aStatus)) { + shouldOpen = false; + } + + if (shouldOpen) { + this._skipClosePrompt = true; + + let file; + if (fp) { + file = fp.file; + } else { + file = Components.classes["@mozilla.org/file/local;1"]. + createInstance(Components.interfaces.nsILocalFile); + let filePath = this.getRecentFiles()[aIndex]; + file.initWithPath(filePath); + } + + this.setFilename(file.path); + this.importFromFile(file, false); + this.setRecentFile(file); + } + }.bind(this)); } }, + /** + * Get recent files. + * + * @return Array + * File paths. + */ + getRecentFiles: function SP_getRecentFiles() + { + let maxRecent = Services.prefs.getIntPref(PREF_RECENT_FILES_MAX); + let branch = Services.prefs. + getBranch("devtools.scratchpad."); + + let filePaths = []; + if (branch.prefHasUserValue("recentFilePaths")) { + filePaths = JSON.parse(branch.getCharPref("recentFilePaths")); + } + + return filePaths; + }, + + /** + * Save a recent file in a JSON parsable string. + * + * @param nsILocalFile aFile + * The nsILocalFile we want to save as a recent file. + */ + setRecentFile: function SP_setRecentFile(aFile) + { + let maxRecent = Services.prefs.getIntPref(PREF_RECENT_FILES_MAX); + if (maxRecent < 1) { + return; + } + + let filePaths = this.getRecentFiles(); + let filesCount = filePaths.length; + let pathIndex = filePaths.indexOf(aFile.path); + + // We are already storing this file in the list of recent files. + if (pathIndex > -1) { + // If it's already the most recent file, we don't have to do anything. + if (pathIndex === (filesCount - 1)) { + // Updating the menu to clear the disabled state from the wrong menuitem + // in rare cases when two or more Scratchpad windows are open and the + // same file has been opened in two or more windows. + this.populateRecentFilesMenu(); + return; + } + + // It is not the most recent file. Remove it from the list, we add it as + // the most recent farther down. + filePaths.splice(pathIndex, 1); + } + // If we are not storing the file and the 'recent files'-list is full, + // remove the oldest file from the list. + else if (filesCount === maxRecent) { + filePaths.shift(); + } + + filePaths.push(aFile.path); + + let branch = Services.prefs. + getBranch("devtools.scratchpad."); + branch.setCharPref("recentFilePaths", JSON.stringify(filePaths)); + return; + }, + + /** + * Populates the 'Open Recent'-menu. + */ + populateRecentFilesMenu: function SP_populateRecentFilesMenu() + { + let maxRecent = Services.prefs.getIntPref(PREF_RECENT_FILES_MAX); + let recentFilesMenu = document.getElementById("sp-open_recent-menu"); + + if (maxRecent < 1) { + recentFilesMenu.setAttribute("hidden", true); + return; + } + + let recentFilesPopup = recentFilesMenu.firstChild; + let filePaths = this.getRecentFiles(); + let filename = this.getState().filename; + + recentFilesMenu.setAttribute("disabled", true); + while (recentFilesPopup.hasChildNodes()) { + recentFilesPopup.removeChild(recentFilesPopup.firstChild); + } + + if (filePaths.length > 0) { + recentFilesMenu.removeAttribute("disabled"); + + // Print out menuitems with the most recent file first. + for (let i = filePaths.length - 1; i >= 0; --i) { + let menuitem = document.createElement("menuitem"); + menuitem.setAttribute("type", "radio"); + menuitem.setAttribute("label", filePaths[i]); + + if (filePaths[i] === filename) { + menuitem.setAttribute("checked", true); + menuitem.setAttribute("disabled", true); + } + + menuitem.setAttribute("oncommand", "Scratchpad.openFile(" + i + ");"); + recentFilesPopup.appendChild(menuitem); + } + + recentFilesPopup.appendChild(document.createElement("menuseparator")); + let clearItems = document.createElement("menuitem"); + clearItems.setAttribute("id", "sp-menu-clear_recent"); + clearItems.setAttribute("label", + this.strings. + GetStringFromName("clearRecentMenuItems.label")); + clearItems.setAttribute("command", "sp-cmd-clearRecentFiles"); + recentFilesPopup.appendChild(clearItems); + } + }, + + /** + * Clear all recent files. + */ + clearRecentFiles: function SP_clearRecentFiles() + { + Services.prefs.clearUserPref("devtools.scratchpad.recentFilePaths"); + }, + + /** + * Handle changes to the 'PREF_RECENT_FILES_MAX'-preference. + */ + handleRecentFileMaxChange: function SP_handleRecentFileMaxChange() + { + let maxRecent = Services.prefs.getIntPref(PREF_RECENT_FILES_MAX); + let menu = document.getElementById("sp-open_recent-menu"); + + // Hide the menu if the 'PREF_RECENT_FILES_MAX'-pref is set to zero or less. + if (maxRecent < 1) { + menu.setAttribute("hidden", true); + } else { + if (menu.hasAttribute("hidden")) { + if (!menu.firstChild.hasChildNodes()) { + this.populateRecentFilesMenu(); + } + + menu.removeAttribute("hidden"); + } + + let filePaths = this.getRecentFiles(); + if (maxRecent < filePaths.length) { + let branch = Services.prefs. + getBranch("devtools.scratchpad."); + let diff = filePaths.length - maxRecent; + filePaths.splice(0, diff); + branch.setCharPref("recentFilePaths", JSON.stringify(filePaths)); + } + } + }, /** * Save the textbox content to the currently open file. * @@ -646,6 +831,7 @@ var Scratchpad = { this.exportToFile(file, true, false, function(aStatus) { if (Components.isSuccessCode(aStatus)) { this.editor.dirty = false; + this.setRecentFile(file); } if (aCallback) { aCallback(aStatus); @@ -671,6 +857,7 @@ var Scratchpad = { this.exportToFile(fp.file, true, false, function(aStatus) { if (Components.isSuccessCode(aStatus)) { this.editor.dirty = false; + this.setRecentFile(fp.file); } if (aCallback) { aCallback(aStatus); @@ -827,6 +1014,9 @@ var Scratchpad = { this.initialized = true; this._triggerObservers("Ready"); + + this.populateRecentFilesMenu(); + PreferenceObserver.init(); }, /** @@ -887,6 +1077,8 @@ var Scratchpad = { this.resetContext(); this.editor.removeEventListener(SourceEditor.EVENTS.DIRTY_CHANGED, this._onDirtyChanged); + PreferenceObserver.uninit(); + this.editor.destroy(); this.editor = null; this.initialized = false; @@ -1063,6 +1255,48 @@ var Scratchpad = { }, }; +/** + * The PreferenceObserver listens for preference changes while Scratchpad is + * running. + */ +var PreferenceObserver = { + _initialized: false, + + init: function PO_init() + { + if (this._initialized) { + return; + } + + this.branch = Services.prefs.getBranch("devtools.scratchpad."); + this.branch.addObserver("", this, false); + this._initialized = true; + }, + + observe: function PO_observe(aMessage, aTopic, aData) + { + if (aTopic != "nsPref:changed") { + return; + } + + if (aData == "recentFilesMax") { + Scratchpad.handleRecentFileMaxChange(); + } + else if (aData == "recentFilePaths") { + Scratchpad.populateRecentFilesMenu(); + } + }, + + uninit: function PO_uninit () { + if (!this.branch) { + return; + } + + this.branch.removeObserver("", this); + this.branch = null; + } +}; + XPCOMUtils.defineLazyGetter(Scratchpad, "strings", function () { return Services.strings.createBundle(SCRATCHPAD_L10N); }); diff --git a/browser/devtools/scratchpad/scratchpad.xul b/browser/devtools/scratchpad/scratchpad.xul index 02230ce9edc..66b2b447560 100644 --- a/browser/devtools/scratchpad/scratchpad.xul +++ b/browser/devtools/scratchpad/scratchpad.xul @@ -30,6 +30,7 @@ + @@ -117,6 +118,11 @@ command="sp-cmd-openFile" key="sp-key-open" accesskey="&openFileCmd.accesskey;"/> + + +