/* * Copyright 2003-2011, Haiku, Inc. All Rights Reserved. * Copyright 2004-2005 yellowTAB GmbH. All Rights Reserverd. * Copyright 2006 Bernd Korz. All Rights Reserved * Distributed under the terms of the MIT License. * * Authors: * Fernando Francisco de Oliveira * Michael Wilber * Michael Pfeiffer * Ryan Leavengood * yellowTAB GmbH * Bernd Korz * Stephan Aßmus * Axel Dörfler, axeld@pinc-software.de */ #include "ShowImageView.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ImageCache.h" #include "ShowImageApp.h" #include "ShowImageWindow.h" using std::nothrow; class PopUpMenu : public BPopUpMenu { public: PopUpMenu(const char* name, BMessenger target); virtual ~PopUpMenu(); private: BMessenger fTarget; }; // the delay time for hiding the cursor in 1/10 seconds (the pulse rate) #define HIDE_CURSOR_DELAY_TIME 20 #define STICKY_ZOOM_DELAY_TIME 5 #define SHOW_IMAGE_ORIENTATION_ATTRIBUTE "ShowImage:orientation" const rgb_color kBorderColor = { 0, 0, 0, 255 }; enum ShowImageView::image_orientation ShowImageView::fTransformation[ImageProcessor::kNumberOfAffineTransformations] [kNumberOfOrientations] = { // rotate 90° {k90, k180, k270, k0, k270V, k0V, k90V, k0H}, // rotate -90° {k270, k0, k90, k180, k90V, k0H, k270V, k0V}, // mirror vertical {k0H, k270V, k0V, k90V, k180, k270, k0, k90}, // mirror horizontal {k0V, k90V, k0H, k270V, k0, k90, k180, k270} }; const rgb_color kAlphaLow = (rgb_color) { 0xbb, 0xbb, 0xbb, 0xff }; const rgb_color kAlphaHigh = (rgb_color) { 0xe0, 0xe0, 0xe0, 0xff }; const uint32 kMsgPopUpMenuClosed = 'pmcl'; inline void blend_colors(uint8* d, uint8 r, uint8 g, uint8 b, uint8 a) { d[0] = ((b - d[0]) * a + (d[0] << 8)) >> 8; d[1] = ((g - d[1]) * a + (d[1] << 8)) >> 8; d[2] = ((r - d[2]) * a + (d[2] << 8)) >> 8; } BBitmap* compose_checker_background(const BBitmap* bitmap) { BBitmap* result = new (nothrow) BBitmap(bitmap); if (result && !result->IsValid()) { delete result; result = NULL; } if (!result) return NULL; uint8* bits = (uint8*)result->Bits(); uint32 bpr = result->BytesPerRow(); uint32 width = result->Bounds().IntegerWidth() + 1; uint32 height = result->Bounds().IntegerHeight() + 1; for (uint32 i = 0; i < height; i++) { uint8* p = bits; for (uint32 x = 0; x < width; x++) { uint8 alpha = p[3]; if (alpha < 255) { p[3] = 255; alpha = 255 - alpha; if (x % 10 >= 5) { if (i % 10 >= 5) blend_colors(p, kAlphaLow.red, kAlphaLow.green, kAlphaLow.blue, alpha); else blend_colors(p, kAlphaHigh.red, kAlphaHigh.green, kAlphaHigh.blue, alpha); } else { if (i % 10 >= 5) blend_colors(p, kAlphaHigh.red, kAlphaHigh.green, kAlphaHigh.blue, alpha); else blend_colors(p, kAlphaLow.red, kAlphaLow.green, kAlphaLow.blue, alpha); } } p += 4; } bits += bpr; } return result; } // #pragma mark - PopUpMenu::PopUpMenu(const char* name, BMessenger target) : BPopUpMenu(name, false, false), fTarget(target) { SetAsyncAutoDestruct(true); } PopUpMenu::~PopUpMenu() { fTarget.SendMessage(kMsgPopUpMenuClosed); } // #pragma mark - ShowImageView::ShowImageView(const char* name, uint32 flags) : BView(name, flags), fBitmapOwner(NULL), fBitmap(NULL), fDisplayBitmap(NULL), fSelectionBitmap(NULL), fZoom(1.0), fScaleBilinear(true), fBitmapLocationInView(0.0, 0.0), fStretchToBounds(false), fForceOriginalSize(false), fHideCursor(false), fScrollingBitmap(false), fCreatingSelection(false), fFirstPoint(0.0, 0.0), fSelectionMode(false), fAnimateSelection(true), fHasSelection(false), fShowCaption(false), fShowingPopUpMenu(false), fHideCursorCountDown(HIDE_CURSOR_DELAY_TIME), fStickyZoomCountDown(0), fIsActiveWin(true), fDefaultCursor(NULL), fGrabCursor(NULL) { ShowImageSettings* settings = my_app->Settings(); if (settings->Lock()) { fStretchToBounds = settings->GetBool("StretchToBounds", fStretchToBounds); fScaleBilinear = settings->GetBool("ScaleBilinear", fScaleBilinear); settings->Unlock(); } fDefaultCursor = new BCursor(B_CURSOR_ID_SYSTEM_DEFAULT); fGrabCursor = new BCursor(B_CURSOR_ID_GRABBING); SetViewColor(B_TRANSPARENT_COLOR); SetHighColor(kBorderColor); SetLowColor(0, 0, 0); } ShowImageView::~ShowImageView() { _DeleteBitmap(); delete fDefaultCursor; delete fGrabCursor; } void ShowImageView::_AnimateSelection(bool enabled) { fAnimateSelection = enabled; } void ShowImageView::Pulse() { // animate marching ants if (fHasSelection && fAnimateSelection && fIsActiveWin) { fSelectionBox.Animate(); fSelectionBox.Draw(this, Bounds()); } if (fHideCursor && !fHasSelection && !fShowingPopUpMenu && fIsActiveWin) { if (fHideCursorCountDown == 0) { // Go negative so this isn't triggered again fHideCursorCountDown--; BPoint mousePos; uint32 buttons; GetMouse(&mousePos, &buttons, false); if (Bounds().Contains(mousePos)) { be_app->ObscureCursor(); // Set current mouse coordinates to avoid the screen saver kicking in ConvertToScreen(&mousePos); set_mouse_position((int32)mousePos.x, (int32)mousePos.y); } } else if (fHideCursorCountDown > 0) fHideCursorCountDown--; } if (fStickyZoomCountDown > 0) fStickyZoomCountDown--; } void ShowImageView::_SendMessageToWindow(BMessage* message) { BMessenger target(Window()); target.SendMessage(message); } void ShowImageView::_SendMessageToWindow(uint32 code) { BMessage message(code); _SendMessageToWindow(&message); } //! send message to parent about new image void ShowImageView::_Notify() { BMessage msg(MSG_UPDATE_STATUS); msg.AddInt32("width", fBitmap->Bounds().IntegerWidth() + 1); msg.AddInt32("height", fBitmap->Bounds().IntegerHeight() + 1); msg.AddInt32("colors", fBitmap->ColorSpace()); _SendMessageToWindow(&msg); FixupScrollBars(); Invalidate(); } void ShowImageView::_UpdateStatusText() { BMessage msg(MSG_UPDATE_STATUS_TEXT); if (fHasSelection) { char size[50]; snprintf(size, sizeof(size), "(%.0fx%.0f)", fSelectionBox.Bounds().Width() + 1.0, fSelectionBox.Bounds().Height() + 1.0); msg.AddString("status", size); } _SendMessageToWindow(&msg); } void ShowImageView::_DeleteBitmap() { _DeleteSelectionBitmap(); if (fDisplayBitmap != fBitmap) delete fDisplayBitmap; fDisplayBitmap = NULL; if (fBitmapOwner != NULL) fBitmapOwner->ReleaseReference(); else delete fBitmap; fBitmapOwner = NULL; fBitmap = NULL; } void ShowImageView::_DeleteSelectionBitmap() { delete fSelectionBitmap; fSelectionBitmap = NULL; } status_t ShowImageView::SetImage(const BMessage* message) { BBitmap* bitmap; entry_ref ref; if (message->FindPointer("bitmap", (void**)&bitmap) != B_OK || message->FindRef("ref", &ref) != B_OK || bitmap == NULL) return B_ERROR; BitmapOwner* bitmapOwner; message->FindPointer("bitmapOwner", (void**)&bitmapOwner); status_t status = SetImage(&ref, bitmap, bitmapOwner); if (status == B_OK) { fFormatDescription = message->FindString("type"); fMimeType = message->FindString("mime"); } return status; } status_t ShowImageView::SetImage(const entry_ref* ref, BBitmap* bitmap, BitmapOwner* bitmapOwner) { // Delete the old one, and clear everything _SetHasSelection(false); fCreatingSelection = false; _DeleteBitmap(); fBitmap = bitmap; fBitmapOwner = bitmapOwner; if (ref == NULL) fCurrentRef.device = -1; else fCurrentRef = *ref; if (fBitmap != NULL) { // prepare the display bitmap if (fBitmap->ColorSpace() == B_RGBA32) fDisplayBitmap = compose_checker_background(fBitmap); if (fDisplayBitmap == NULL) fDisplayBitmap = fBitmap; BNode node(ref); // restore orientation int32 orientation; fImageOrientation = k0; if (node.ReadAttr(SHOW_IMAGE_ORIENTATION_ATTRIBUTE, B_INT32_TYPE, 0, &orientation, sizeof(orientation)) == sizeof(orientation)) { orientation &= 255; switch (orientation) { case k0: break; case k90: _DoImageOperation(ImageProcessor::kRotateClockwise, true); break; case k180: _DoImageOperation(ImageProcessor::kRotateClockwise, true); _DoImageOperation(ImageProcessor::kRotateClockwise, true); break; case k270: _DoImageOperation(ImageProcessor::kRotateCounterClockwise, true); break; case k0V: _DoImageOperation(ImageProcessor::ImageProcessor::kFlipTopToBottom, true); break; case k90V: _DoImageOperation(ImageProcessor::kRotateClockwise, true); _DoImageOperation(ImageProcessor::ImageProcessor::kFlipTopToBottom, true); break; case k0H: _DoImageOperation(ImageProcessor::ImageProcessor::kFlipLeftToRight, true); break; case k270V: _DoImageOperation(ImageProcessor::kRotateCounterClockwise, true); _DoImageOperation(ImageProcessor::ImageProcessor::kFlipTopToBottom, true); break; } } } BPath path(ref); fCaption = path.Path(); fFormatDescription = "Bitmap"; fMimeType = "image/x-be-bitmap"; be_roster->AddToRecentDocuments(ref, kApplicationSignature); FitToBounds(); _Notify(); return B_OK; } BPoint ShowImageView::ImageToView(BPoint p) const { p.x = floorf(fZoom * p.x + fBitmapLocationInView.x); p.y = floorf(fZoom * p.y + fBitmapLocationInView.y); return p; } BPoint ShowImageView::ViewToImage(BPoint p) const { p.x = floorf((p.x - fBitmapLocationInView.x) / fZoom); p.y = floorf((p.y - fBitmapLocationInView.y) / fZoom); return p; } BRect ShowImageView::ImageToView(BRect r) const { BPoint leftTop(ImageToView(BPoint(r.left, r.top))); BPoint rightBottom(r.right, r.bottom); rightBottom += BPoint(1, 1); rightBottom = ImageToView(rightBottom); rightBottom -= BPoint(1, 1); return BRect(leftTop.x, leftTop.y, rightBottom.x, rightBottom.y); } void ShowImageView::ConstrainToImage(BPoint& point) const { point.ConstrainTo(fBitmap->Bounds()); } void ShowImageView::ConstrainToImage(BRect& rect) const { rect = rect & fBitmap->Bounds(); } void ShowImageView::SetShowCaption(bool show) { if (fShowCaption != show) { fShowCaption = show; _UpdateCaption(); } } void ShowImageView::SetStretchToBounds(bool enable) { if (fStretchToBounds != enable) { _SettingsSetBool("StretchToBounds", enable); fStretchToBounds = enable; if (enable || fZoom > 1.0) FitToBounds(); } } void ShowImageView::SetHideIdlingCursor(bool hide) { fHideCursor = hide; } BBitmap* ShowImageView::Bitmap() { return fBitmap; } void ShowImageView::SetScaleBilinear(bool enabled) { if (fScaleBilinear != enabled) { _SettingsSetBool("ScaleBilinear", enabled); fScaleBilinear = enabled; Invalidate(); } } void ShowImageView::AttachedToWindow() { FitToBounds(); FixupScrollBars(); } void ShowImageView::FrameResized(float width, float height) { FixupScrollBars(); } float ShowImageView::_FitToBoundsZoom() const { if (fBitmap == NULL) return 1.0f; // the width/height of the bitmap (in pixels) float bitmapWidth = fBitmap->Bounds().Width() + 1; float bitmapHeight = fBitmap->Bounds().Height() + 1; // the available width/height for layouting the bitmap (in pixels) float width = Bounds().Width() + 1; float height = Bounds().Height() + 1; float zoom = width / bitmapWidth; if (zoom * bitmapHeight <= height) return zoom; return height / bitmapHeight; } BRect ShowImageView::_AlignBitmap() { BRect rect(fBitmap->Bounds()); // the width/height of the bitmap (in pixels) float bitmapWidth = rect.Width() + 1; float bitmapHeight = rect.Height() + 1; // the available width/height for layouting the bitmap (in pixels) float width = Bounds().Width() + 1; float height = Bounds().Height() + 1; if (width == 0 || height == 0) return rect; // zoom image rect.right = floorf(bitmapWidth * fZoom) - 1; rect.bottom = floorf(bitmapHeight * fZoom) - 1; // update the bitmap size after the zoom bitmapWidth = rect.Width() + 1.0; bitmapHeight = rect.Height() + 1.0; // always align in the center if the bitmap is smaller than the window if (width > bitmapWidth) rect.OffsetBy(floorf((width - bitmapWidth) / 2.0), 0); if (height > bitmapHeight) rect.OffsetBy(0, floorf((height - bitmapHeight) / 2.0)); return rect; } void ShowImageView::_DrawBackground(BRect border) { BRect bounds(Bounds()); // top FillRect(BRect(0, 0, bounds.right, border.top - 1), B_SOLID_LOW); // left FillRect(BRect(0, border.top, border.left - 1, border.bottom), B_SOLID_LOW); // right FillRect(BRect(border.right + 1, border.top, bounds.right, border.bottom), B_SOLID_LOW); // bottom FillRect(BRect(0, border.bottom + 1, bounds.right, bounds.bottom), B_SOLID_LOW); } void ShowImageView::_LayoutCaption(BFont& font, BPoint& pos, BRect& rect) { font_height fontHeight; float width, height; BRect bounds(Bounds()); font = be_plain_font; width = font.StringWidth(fCaption.String()); font.GetHeight(&fontHeight); height = fontHeight.ascent + fontHeight.descent; // center text horizontally pos.x = (bounds.left + bounds.right - width) / 2; // flush bottom pos.y = bounds.bottom - fontHeight.descent - 7; // background rectangle rect.Set(0, 0, width + 4, height + 4); rect.OffsetTo(pos); rect.OffsetBy(-2, -2 - fontHeight.ascent); // -2 for border } void ShowImageView::_DrawCaption() { BFont font; BPoint position; BRect rect; _LayoutCaption(font, position, rect); PushState(); // draw background SetDrawingMode(B_OP_ALPHA); SetHighColor(255, 255, 255, 160); FillRect(rect); // draw text SetDrawingMode(B_OP_OVER); SetFont(&font); SetLowColor(B_TRANSPARENT_COLOR); SetHighColor(0, 0, 0); DrawString(fCaption.String(), position); PopState(); } void ShowImageView::_UpdateCaption() { BFont font; BPoint pos; BRect rect; _LayoutCaption(font, pos, rect); // draw over portion of image where caption is located BRegion clip(rect); PushState(); ConstrainClippingRegion(&clip); Draw(rect); PopState(); } void ShowImageView::_DrawImage(BRect rect) { // TODO: fix composing of fBitmap with other bitmaps // with regard to alpha channel if (!fDisplayBitmap) fDisplayBitmap = fBitmap; uint32 options = fScaleBilinear ? B_FILTER_BITMAP_BILINEAR : 0; DrawBitmap(fDisplayBitmap, fDisplayBitmap->Bounds(), rect, options); } void ShowImageView::Draw(BRect updateRect) { if (fBitmap == NULL) return; if (IsPrinting()) { DrawBitmap(fBitmap); return; } BRect rect = _AlignBitmap(); fBitmapLocationInView.x = floorf(rect.left); fBitmapLocationInView.y = floorf(rect.top); _DrawBackground(rect); _DrawImage(rect); if (fShowCaption) _DrawCaption(); if (fHasSelection) { if (fSelectionBitmap != NULL) { BRect srcRect; BRect dstRect; _GetSelectionMergeRects(srcRect, dstRect); dstRect = ImageToView(dstRect); DrawBitmap(fSelectionBitmap, srcRect, dstRect); } fSelectionBox.Draw(this, updateRect); } } BBitmap* ShowImageView::_CopySelection(uchar alpha, bool imageSize) { bool hasAlpha = alpha != 255; if (!fHasSelection) return NULL; BRect rect = fSelectionBox.Bounds().OffsetToCopy(B_ORIGIN); if (!imageSize) { // scale image to view size rect.right = floorf((rect.right + 1.0) * fZoom - 1.0); rect.bottom = floorf((rect.bottom + 1.0) * fZoom - 1.0); } BView view(rect, NULL, B_FOLLOW_NONE, B_WILL_DRAW); BBitmap* bitmap = new(nothrow) BBitmap(rect, hasAlpha ? B_RGBA32 : fBitmap->ColorSpace(), true); if (bitmap == NULL || !bitmap->IsValid()) { delete bitmap; return NULL; } if (bitmap->Lock()) { bitmap->AddChild(&view); #ifdef __HAIKU__ // On Haiku, B_OP_SUBSTRACT does not affect alpha like it did on BeOS. // Don't know if it's better to fix it or not (stippi). if (hasAlpha) { view.SetHighColor(0, 0, 0, 0); view.FillRect(view.Bounds()); view.SetDrawingMode(B_OP_ALPHA); view.SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_COMPOSITE); view.SetHighColor(0, 0, 0, alpha); } if (fSelectionBitmap) { view.DrawBitmap(fSelectionBitmap, fSelectionBitmap->Bounds().OffsetToCopy(B_ORIGIN), rect); } else view.DrawBitmap(fBitmap, fCopyFromRect, rect); #else if (fSelectionBitmap) { view.DrawBitmap(fSelectionBitmap, fSelectionBitmap->Bounds().OffsetToCopy(B_ORIGIN), rect); } else view.DrawBitmap(fBitmap, fCopyFromRect, rect); if (hasAlpha) { view.SetDrawingMode(B_OP_SUBTRACT); view.SetHighColor(0, 0, 0, 255 - alpha); view.FillRect(rect, B_SOLID_HIGH); } #endif view.Sync(); bitmap->RemoveChild(&view); bitmap->Unlock(); } return bitmap; } bool ShowImageView::_AddSupportedTypes(BMessage* msg, BBitmap* bitmap) { BTranslatorRoster* roster = BTranslatorRoster::Default(); if (roster == NULL) return false; // add the current image mime first, will make it the preferred format on // left mouse drag msg->AddString("be:types", fMimeType); msg->AddString("be:filetypes", fMimeType); msg->AddString("be:type_descriptions", fFormatDescription); bool foundOther = false; bool foundCurrent = false; int32 infoCount; translator_info* info; BBitmapStream stream(bitmap); if (roster->GetTranslators(&stream, NULL, &info, &infoCount) == B_OK) { for (int32 i = 0; i < infoCount; i++) { const translation_format* formats; int32 count; roster->GetOutputFormats(info[i].translator, &formats, &count); for (int32 j = 0; j < count; j++) { if (fMimeType == formats[j].MIME) foundCurrent = true; else if (strcmp(formats[j].MIME, "image/x-be-bitmap") != 0) { foundOther = true; // needed to send data in message msg->AddString("be:types", formats[j].MIME); // needed to pass data via file msg->AddString("be:filetypes", formats[j].MIME); msg->AddString("be:type_descriptions", formats[j].name); } } } } stream.DetachBitmap(&bitmap); if (!foundCurrent) { msg->RemoveData("be:types", 0); msg->RemoveData("be:filetypes", 0); msg->RemoveData("be:type_descriptions", 0); } return foundOther || foundCurrent; } void ShowImageView::_BeginDrag(BPoint sourcePoint) { BBitmap* bitmap = _CopySelection(128, false); if (bitmap == NULL) return; SetMouseEventMask(B_POINTER_EVENTS); // fill the drag message BMessage drag(B_SIMPLE_DATA); drag.AddInt32("be:actions", B_COPY_TARGET); drag.AddString("be:clip_name", "Bitmap Clip"); // ShowImage specific fields drag.AddPoint("be:_source_point", sourcePoint); drag.AddRect("be:_frame", fSelectionBox.Bounds()); if (_AddSupportedTypes(&drag, bitmap)) { // we also support "Passing Data via File" protocol drag.AddString("be:types", B_FILE_MIME_TYPE); // avoid flickering of dragged bitmap caused by drawing into the window _AnimateSelection(false); // only use a transparent bitmap on selections less than 400x400 // (taking into account zooming) BRect selectionRect = fSelectionBox.Bounds(); if (selectionRect.Width() * fZoom < 400.0 && selectionRect.Height() * fZoom < 400.0) { sourcePoint -= selectionRect.LeftTop(); sourcePoint.x *= fZoom; sourcePoint.y *= fZoom; // DragMessage takes ownership of bitmap DragMessage(&drag, bitmap, B_OP_ALPHA, sourcePoint); bitmap = NULL; } else { delete bitmap; // Offset and scale the rect BRect rect(selectionRect); rect = ImageToView(rect); rect.InsetBy(-1, -1); DragMessage(&drag, rect); } } } bool ShowImageView::_OutputFormatForType(BBitmap* bitmap, const char* type, translation_format* format) { bool found = false; BTranslatorRoster* roster = BTranslatorRoster::Default(); if (roster == NULL) return false; BBitmapStream stream(bitmap); translator_info* outInfo; int32 outNumInfo; if (roster->GetTranslators(&stream, NULL, &outInfo, &outNumInfo) == B_OK) { for (int32 i = 0; i < outNumInfo; i++) { const translation_format* formats; int32 formatCount; roster->GetOutputFormats(outInfo[i].translator, &formats, &formatCount); for (int32 j = 0; j < formatCount; j++) { if (strcmp(formats[j].MIME, type) == 0) { *format = formats[j]; found = true; break; } } } } stream.DetachBitmap(&bitmap); return found; } #undef B_TRANSLATION_CONTEXT #define B_TRANSLATION_CONTEXT "SaveToFile" void ShowImageView::SaveToFile(BDirectory* dir, const char* name, BBitmap* bitmap, const translation_format* format) { if (bitmap == NULL) { // If no bitmap is supplied, write out the whole image bitmap = fBitmap; } BBitmapStream stream(bitmap); bool loop = true; while (loop) { BTranslatorRoster* roster = BTranslatorRoster::Default(); if (!roster) break; // write data BFile file(dir, name, B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE); if (file.InitCheck() != B_OK) break; if (roster->Translate(&stream, NULL, NULL, &file, format->type) < B_OK) break; // set mime type BNodeInfo info(&file); if (info.InitCheck() == B_OK) info.SetType(format->MIME); loop = false; // break out of loop gracefully (indicates no errors) } if (loop) { // If loop terminated because of a break, there was an error char buffer[512]; snprintf(buffer, sizeof(buffer), B_TRANSLATE("The file '%s' could not " "be written."), name); BAlert* palert = new BAlert("", buffer, B_TRANSLATE("OK")); palert->SetFlags(palert->Flags() | B_CLOSE_ON_ESCAPE); palert->Go(); } stream.DetachBitmap(&bitmap); // Don't allow the bitmap to be deleted, this is // especially important when using fBitmap as the bitmap } void ShowImageView::_SendInMessage(BMessage* msg, BBitmap* bitmap, translation_format* format) { BMessage reply(B_MIME_DATA); BBitmapStream stream(bitmap); // destructor deletes bitmap BTranslatorRoster* roster = BTranslatorRoster::Default(); BMallocIO memStream; if (roster->Translate(&stream, NULL, NULL, &memStream, format->type) == B_OK) { reply.AddData(format->MIME, B_MIME_TYPE, memStream.Buffer(), memStream.BufferLength()); msg->SendReply(&reply); } } void ShowImageView::_HandleDrop(BMessage* msg) { entry_ref dirRef; BString name, type; bool saveToFile = msg->FindString("be:filetypes", &type) == B_OK && msg->FindRef("directory", &dirRef) == B_OK && msg->FindString("name", &name) == B_OK; bool sendInMessage = !saveToFile && msg->FindString("be:types", &type) == B_OK; BBitmap* bitmap = _CopySelection(); if (bitmap == NULL) return; translation_format format; if (!_OutputFormatForType(bitmap, type.String(), &format)) { delete bitmap; return; } if (saveToFile) { BDirectory dir(&dirRef); SaveToFile(&dir, name.String(), bitmap, &format); delete bitmap; } else if (sendInMessage) { _SendInMessage(msg, bitmap, &format); } else { delete bitmap; } } void ShowImageView::_ScrollBitmap(BPoint point) { point = ConvertToScreen(point); BPoint delta = fFirstPoint - point; fFirstPoint = point; _ScrollRestrictedBy(delta.x, delta.y); } void ShowImageView::_GetMergeRects(BBitmap* merge, BRect selection, BRect& srcRect, BRect& dstRect) { // Constrain dstRect to target image size and apply the same edge offsets // to the srcRect. dstRect = selection; BRect clippedDstRect(dstRect); ConstrainToImage(clippedDstRect); srcRect = merge->Bounds().OffsetToCopy(B_ORIGIN); srcRect.left += clippedDstRect.left - dstRect.left; srcRect.top += clippedDstRect.top - dstRect.top; srcRect.right += clippedDstRect.right - dstRect.right; srcRect.bottom += clippedDstRect.bottom - dstRect.bottom; dstRect = clippedDstRect; } void ShowImageView::_GetSelectionMergeRects(BRect& srcRect, BRect& dstRect) { _GetMergeRects(fSelectionBitmap, fSelectionBox.Bounds(), srcRect, dstRect); } void ShowImageView::_MergeWithBitmap(BBitmap* merge, BRect selection) { BView view(fBitmap->Bounds(), NULL, B_FOLLOW_NONE, B_WILL_DRAW); BBitmap* bitmap = new(nothrow) BBitmap(fBitmap->Bounds(), fBitmap->ColorSpace(), true); if (bitmap == NULL || !bitmap->IsValid()) { delete bitmap; return; } if (bitmap->Lock()) { bitmap->AddChild(&view); view.DrawBitmap(fBitmap, fBitmap->Bounds()); BRect srcRect; BRect dstRect; _GetMergeRects(merge, selection, srcRect, dstRect); view.DrawBitmap(merge, srcRect, dstRect); view.Sync(); bitmap->RemoveChild(&view); bitmap->Unlock(); _DeleteBitmap(); fBitmap = bitmap; _SendMessageToWindow(MSG_MODIFIED); } else delete bitmap; } void ShowImageView::MouseDown(BPoint position) { MakeFocus(true); BPoint point = ViewToImage(position); int32 clickCount = 0; uint32 buttons = 0; if (Window() != NULL && Window()->CurrentMessage() != NULL) { clickCount = Window()->CurrentMessage()->FindInt32("clicks"); buttons = Window()->CurrentMessage()->FindInt32("buttons"); } // Using clickCount >= 2 and the modulo 2 accounts for quickly repeated // double-clicks if (buttons == B_PRIMARY_MOUSE_BUTTON && clickCount >= 2 && clickCount % 2 == 0) { Window()->PostMessage(MSG_FULL_SCREEN); return; } if (fHasSelection && fSelectionBox.Bounds().Contains(point) && (buttons & (B_PRIMARY_MOUSE_BUTTON | B_SECONDARY_MOUSE_BUTTON)) != 0) { if (!fSelectionBitmap) fSelectionBitmap = _CopySelection(); _BeginDrag(point); } else if (buttons == B_PRIMARY_MOUSE_BUTTON && (fSelectionMode || (modifiers() & (B_COMMAND_KEY | B_CONTROL_KEY)) != 0)) { // begin new selection _SetHasSelection(true); fCreatingSelection = true; SetMouseEventMask(B_POINTER_EVENTS, B_NO_POINTER_HISTORY); ConstrainToImage(point); fFirstPoint = point; fCopyFromRect.Set(point.x, point.y, point.x, point.y); fSelectionBox.SetBounds(this, fCopyFromRect); Invalidate(); } else if (buttons == B_SECONDARY_MOUSE_BUTTON) { _ShowPopUpMenu(ConvertToScreen(position)); } else if (buttons == B_PRIMARY_MOUSE_BUTTON || buttons == B_TERTIARY_MOUSE_BUTTON) { // move image in window SetMouseEventMask(B_POINTER_EVENTS, B_NO_POINTER_HISTORY); fScrollingBitmap = true; fFirstPoint = ConvertToScreen(position); be_app->SetCursor(fGrabCursor); } } void ShowImageView::_UpdateSelectionRect(BPoint point, bool final) { BRect oldSelection = fCopyFromRect; point = ViewToImage(point); ConstrainToImage(point); fCopyFromRect.left = min_c(fFirstPoint.x, point.x); fCopyFromRect.right = max_c(fFirstPoint.x, point.x); fCopyFromRect.top = min_c(fFirstPoint.y, point.y); fCopyFromRect.bottom = max_c(fFirstPoint.y, point.y); fSelectionBox.SetBounds(this, fCopyFromRect); if (final) { // selection must be at least 2 pixels wide or 2 pixels tall if (fCopyFromRect.Width() < 1.0 && fCopyFromRect.Height() < 1.0) _SetHasSelection(false); } else _UpdateStatusText(); if (oldSelection != fCopyFromRect || !fHasSelection) { BRect updateRect; updateRect = oldSelection | fCopyFromRect; updateRect = ImageToView(updateRect); updateRect.InsetBy(-1, -1); Invalidate(updateRect); } } void ShowImageView::MouseMoved(BPoint point, uint32 state, const BMessage* message) { fHideCursorCountDown = HIDE_CURSOR_DELAY_TIME; if (fHideCursor) { // Show toolbar when mouse hits top 15 pixels, hide otherwise _ShowToolBarIfEnabled(ConvertToScreen(point).y <= 15); } if (fCreatingSelection) _UpdateSelectionRect(point, false); else if (fScrollingBitmap) _ScrollBitmap(point); } void ShowImageView::MouseUp(BPoint point) { if (fCreatingSelection) { _UpdateSelectionRect(point, true); fCreatingSelection = false; } else if (fScrollingBitmap) { _ScrollBitmap(point); fScrollingBitmap = false; be_app->SetCursor(fDefaultCursor); } _AnimateSelection(true); } float ShowImageView::_LimitToRange(float v, orientation o, bool absolute) { BScrollBar* psb = ScrollBar(o); if (psb) { float min, max, pos; pos = v; if (!absolute) pos += psb->Value(); psb->GetRange(&min, &max); if (pos < min) pos = min; else if (pos > max) pos = max; v = pos; if (!absolute) v -= psb->Value(); } return v; } void ShowImageView::_ScrollRestricted(float x, float y, bool absolute) { if (x != 0) x = _LimitToRange(x, B_HORIZONTAL, absolute); if (y != 0) y = _LimitToRange(y, B_VERTICAL, absolute); // We invalidate before we scroll to avoid the caption messing up the // image, and to prevent it from flickering if (fShowCaption) Invalidate(); ScrollBy(x, y); } // XXX method is not unused void ShowImageView::_ScrollRestrictedTo(float x, float y) { _ScrollRestricted(x, y, true); } void ShowImageView::_ScrollRestrictedBy(float x, float y) { _ScrollRestricted(x, y, false); } void ShowImageView::KeyDown(const char* bytes, int32 numBytes) { if (numBytes != 1) { BView::KeyDown(bytes, numBytes); return; } bool shiftKeyDown = (modifiers() & B_SHIFT_KEY) != 0; switch (*bytes) { case B_DOWN_ARROW: if (shiftKeyDown) _ScrollRestrictedBy(0, 10); else _SendMessageToWindow(MSG_FILE_NEXT); break; case B_RIGHT_ARROW: if (shiftKeyDown) _ScrollRestrictedBy(10, 0); else _SendMessageToWindow(MSG_FILE_NEXT); break; case B_UP_ARROW: if (shiftKeyDown) _ScrollRestrictedBy(0, -10); else _SendMessageToWindow(MSG_FILE_PREV); break; case B_LEFT_ARROW: if (shiftKeyDown) _ScrollRestrictedBy(-10, 0); else _SendMessageToWindow(MSG_FILE_PREV); break; case B_BACKSPACE: _SendMessageToWindow(MSG_FILE_PREV); break; case B_HOME: break; case B_END: break; case B_SPACE: _ToggleSlideShow(); break; case B_ESCAPE: // stop slide show _StopSlideShow(); _ExitFullScreen(); ClearSelection(); break; case B_DELETE: if (fHasSelection) ClearSelection(); else _SendMessageToWindow(kMsgDeleteCurrentFile); break; case '0': FitToBounds(); break; case '1': SetZoom(1.0f); break; case '+': case '=': ZoomIn(); break; case '-': ZoomOut(); break; case '[': Rotate(270); break; case ']': Rotate(90); break; } } void ShowImageView::_MouseWheelChanged(BMessage* message) { // The BeOS driver does not currently support // X wheel scrolling, therefore, deltaX is zero. // |deltaY| is the number of notches scrolled up or down. // When the wheel is scrolled down (towards the user) deltaY > 0 // When the wheel is scrolled up (away from the user) deltaY < 0 const float kscrollBy = 40; float deltaY; float deltaX; float x = 0; float y = 0; if (message->FindFloat("be:wheel_delta_x", &deltaX) == B_OK) x = deltaX * kscrollBy; if (message->FindFloat("be:wheel_delta_y", &deltaY) == B_OK) y = deltaY * kscrollBy; if ((modifiers() & B_SHIFT_KEY) != 0) { // scroll up and down _ScrollRestrictedBy(x, y); } else if ((modifiers() & B_CONTROL_KEY) != 0) { // scroll left and right _ScrollRestrictedBy(y, x); } else { // zoom at location BPoint where; uint32 buttons; GetMouse(&where, &buttons); if (fStickyZoomCountDown <= 0) { if (deltaY < 0) ZoomIn(where); else if (deltaY > 0) ZoomOut(where); if (fZoom == 1.0) fStickyZoomCountDown = STICKY_ZOOM_DELAY_TIME; } } } void ShowImageView::_ShowPopUpMenu(BPoint screen) { if (!fShowingPopUpMenu) { PopUpMenu* menu = new PopUpMenu("PopUpMenu", this); ShowImageWindow* window = dynamic_cast(Window()); if (window != NULL) window->BuildContextMenu(menu); menu->Go(screen, true, true, true); fShowingPopUpMenu = true; } } void ShowImageView::_SettingsSetBool(const char* name, bool value) { ShowImageSettings* settings; settings = my_app->Settings(); if (settings->Lock()) { settings->SetBool(name, value); settings->Unlock(); } } void ShowImageView::MessageReceived(BMessage* message) { switch (message->what) { case B_COPY_TARGET: _HandleDrop(message); break; case B_MOUSE_WHEEL_CHANGED: _MouseWheelChanged(message); break; case kMsgPopUpMenuClosed: fShowingPopUpMenu = false; break; default: BView::MessageReceived(message); break; } } void ShowImageView::FixupScrollBar(orientation o, float bitmapLength, float viewLength) { float prop, range; BScrollBar* psb; psb = ScrollBar(o); if (psb) { range = bitmapLength - viewLength; if (range < 0.0) range = 0.0; prop = viewLength / bitmapLength; if (prop > 1.0) prop = 1.0; psb->SetRange(0, range); psb->SetProportion(prop); psb->SetSteps(10, 100); } } void ShowImageView::FixupScrollBars() { BRect viewRect = Bounds(); BRect bitmapRect; if (fBitmap != NULL) { bitmapRect = _AlignBitmap(); bitmapRect.OffsetTo(0, 0); } FixupScrollBar(B_HORIZONTAL, bitmapRect.Width(), viewRect.Width()); FixupScrollBar(B_VERTICAL, bitmapRect.Height(), viewRect.Height()); } void ShowImageView::SetSelectionMode(bool selectionMode) { // The mode only has an effect in MouseDown() fSelectionMode = selectionMode; } void ShowImageView::SelectAll() { fCopyFromRect.Set(0, 0, fBitmap->Bounds().Width(), fBitmap->Bounds().Height()); fSelectionBox.SetBounds(this, fCopyFromRect); _SetHasSelection(true); Invalidate(); } void ShowImageView::ClearSelection() { if (!fHasSelection) return; _SetHasSelection(false); Invalidate(); } void ShowImageView::_SetHasSelection(bool hasSelection) { _DeleteSelectionBitmap(); fHasSelection = hasSelection; _UpdateStatusText(); BMessage msg(MSG_SELECTION); msg.AddBool("has_selection", fHasSelection); _SendMessageToWindow(&msg); } void ShowImageView::CopySelectionToClipboard() { if (!fHasSelection || !be_clipboard->Lock()) return; be_clipboard->Clear(); BMessage* data = be_clipboard->Data(); if (data != NULL) { BBitmap* bitmap = _CopySelection(); if (bitmap != NULL) { BMessage bitmapArchive; bitmap->Archive(&bitmapArchive); // NOTE: Possibly "image/x-be-bitmap" is more correct. // This works with WonderBrush, though, which in turn had been // tested with other apps. data->AddMessage("image/bitmap", &bitmapArchive); data->AddPoint("be:location", fSelectionBox.Bounds().LeftTop()); delete bitmap; be_clipboard->Commit(); } } be_clipboard->Unlock(); } void ShowImageView::SetZoom(float zoom, BPoint where) { float fitToBoundsZoom = _FitToBoundsZoom(); if (zoom > 32) zoom = 32; if (zoom < fitToBoundsZoom / 2 && zoom < 0.25) zoom = min_c(fitToBoundsZoom / 2, 0.25); if (zoom == fZoom) { // window size might have changed FixupScrollBars(); return; } // Invalidate before scrolling, as that prevents the app_server // to do the scrolling server side Invalidate(); // zoom to center if not otherwise specified BPoint offset; if (where.x == -1) { where.Set(Bounds().Width() / 2, Bounds().Height() / 2); offset = where; where += Bounds().LeftTop(); } else offset = where - Bounds().LeftTop(); float oldZoom = fZoom; fZoom = zoom; FixupScrollBars(); if (fBitmap != NULL) { offset.x = (int)(where.x * fZoom / oldZoom + 0.5) - offset.x; offset.y = (int)(where.y * fZoom / oldZoom + 0.5) - offset.y; ScrollTo(offset); } BMessage message(MSG_UPDATE_STATUS_ZOOM); message.AddFloat("zoom", fZoom); _SendMessageToWindow(&message); } void ShowImageView::ZoomIn(BPoint where) { // snap zoom to "fit to bounds", and "original size" float zoom = fZoom * 1.2; float zoomSnap = fZoom * 1.25; float fitToBoundsZoom = _FitToBoundsZoom(); if (fZoom < fitToBoundsZoom - 0.001 && zoomSnap > fitToBoundsZoom) zoom = fitToBoundsZoom; if (fZoom < 1.0 && zoomSnap > 1.0) zoom = 1.0; SetZoom(zoom, where); } void ShowImageView::ZoomOut(BPoint where) { // snap zoom to "fit to bounds", and "original size" float zoom = fZoom / 1.2; float zoomSnap = fZoom / 1.25; float fitToBoundsZoom = _FitToBoundsZoom(); if (fZoom > fitToBoundsZoom + 0.001 && zoomSnap < fitToBoundsZoom) zoom = fitToBoundsZoom; if (fZoom > 1.0 && zoomSnap < 1.0) zoom = 1.0; SetZoom(zoom, where); } /*! Fits the image to the view bounds. */ void ShowImageView::FitToBounds() { if (fBitmap == NULL) return; float fitToBoundsZoom = _FitToBoundsZoom(); if ((!fStretchToBounds && fitToBoundsZoom > 1.0f) || fForceOriginalSize) SetZoom(1.0f); else SetZoom(fitToBoundsZoom); FixupScrollBars(); } void ShowImageView::_DoImageOperation(ImageProcessor::operation op, bool quiet) { BMessenger msgr; ImageProcessor imageProcessor(op, fBitmap, msgr, 0); imageProcessor.Start(false); BBitmap* bm = imageProcessor.DetachBitmap(); if (bm == NULL) { // operation failed return; } // update orientation state if (op != ImageProcessor::kInvert) { // Note: If one of these fails, check its definition in class ImageProcessor. // ASSERT(ImageProcessor::kRotateClockwise < // ImageProcessor::kNumberOfAffineTransformations); // ASSERT(ImageProcessor::kRotateCounterClockwise < // ImageProcessor::kNumberOfAffineTransformations); // ASSERT(ImageProcessor::kFlipLeftToRight < // ImageProcessor::kNumberOfAffineTransformations); // ASSERT(ImageProcessor::kFlipTopToBottom < // ImageProcessor::kNumberOfAffineTransformations); fImageOrientation = fTransformation[op][fImageOrientation]; } if (!quiet) { // write orientation state BNode node(&fCurrentRef); int32 orientation = fImageOrientation; if (orientation != k0) { node.WriteAttr(SHOW_IMAGE_ORIENTATION_ATTRIBUTE, B_INT32_TYPE, 0, &orientation, sizeof(orientation)); } else node.RemoveAttr(SHOW_IMAGE_ORIENTATION_ATTRIBUTE); } // set new bitmap _DeleteBitmap(); fBitmap = bm; if (fBitmap->ColorSpace() == B_RGBA32) fDisplayBitmap = compose_checker_background(fBitmap); if (!quiet) { // remove selection _SetHasSelection(false); _Notify(); } } //! Image operation initiated by user void ShowImageView::_UserDoImageOperation(ImageProcessor::operation op, bool quiet) { _DoImageOperation(op, quiet); } void ShowImageView::Rotate(int degree) { _UserDoImageOperation(degree == 90 ? ImageProcessor::kRotateClockwise : ImageProcessor::kRotateCounterClockwise); FitToBounds(); } void ShowImageView::Flip(bool vertical) { if (vertical) _UserDoImageOperation(ImageProcessor::kFlipLeftToRight); else _UserDoImageOperation(ImageProcessor::kFlipTopToBottom); } void ShowImageView::ResizeImage(int w, int h) { if (fBitmap == NULL || w < 1 || h < 1) return; Scaler scaler(fBitmap, BRect(0, 0, w - 1, h - 1), BMessenger(), 0, false); scaler.Start(false); BBitmap* scaled = scaler.DetachBitmap(); if (scaled == NULL) { // operation failed return; } // remove selection _SetHasSelection(false); _DeleteBitmap(); fBitmap = scaled; _SendMessageToWindow(MSG_MODIFIED); _Notify(); } void ShowImageView::_SetIcon(bool clear, icon_size which) { const int32 size = be_control_look->ComposeIconSize(which).IntegerWidth() + 1; BRect rect(fBitmap->Bounds()); float s; s = size / (rect.Width() + 1.0); if (s * (rect.Height() + 1.0) <= size) { rect.right = size - 1; rect.bottom = static_cast(s * (rect.Height() + 1.0)) - 1; // center vertically rect.OffsetBy(0, (size - rect.IntegerHeight()) / 2); } else { s = size / (rect.Height() + 1.0); rect.right = static_cast(s * (rect.Width() + 1.0)) - 1; rect.bottom = size - 1; // center horizontally rect.OffsetBy((size - rect.IntegerWidth()) / 2, 0); } // scale bitmap to thumbnail size BMessenger msgr; Scaler scaler(fBitmap, rect, msgr, 0, true); BBitmap* thumbnail = scaler.GetBitmap(); scaler.Start(false); ASSERT(thumbnail->ColorSpace() == B_CMAP8); // create icon from thumbnail BBitmap icon(BRect(0, 0, size - 1, size - 1), B_CMAP8); memset(icon.Bits(), B_TRANSPARENT_MAGIC_CMAP8, icon.BitsLength()); BScreen screen; const uchar* src = (uchar*)thumbnail->Bits(); uchar* dest = (uchar*)icon.Bits(); const int32 srcBPR = thumbnail->BytesPerRow(); const int32 destBPR = icon.BytesPerRow(); const int32 deltaX = (int32)rect.left; const int32 deltaY = (int32)rect.top; for (int32 y = 0; y <= rect.IntegerHeight(); y++) { for (int32 x = 0; x <= rect.IntegerWidth(); x++) { const uchar* s = src + y * srcBPR + x; uchar* d = dest + (y + deltaY) * destBPR + (x + deltaX); *d = *s; } } // set icon BNode node(&fCurrentRef); BNodeInfo info(&node); info.SetIcon(clear ? NULL : &icon, which); } void ShowImageView::SetIcon(bool clear) { _SetIcon(clear, B_MINI_ICON); _SetIcon(clear, B_LARGE_ICON); } void ShowImageView::_ToggleSlideShow() { _SendMessageToWindow(MSG_SLIDE_SHOW); } void ShowImageView::_StopSlideShow() { _SendMessageToWindow(kMsgStopSlideShow); } void ShowImageView::_ExitFullScreen() { be_app->ShowCursor(); _SendMessageToWindow(MSG_EXIT_FULL_SCREEN); } void ShowImageView::_ShowToolBarIfEnabled(bool show) { BMessage message(kShowToolBarIfEnabled); message.AddBool("show", show); Window()->PostMessage(&message); } void ShowImageView::WindowActivated(bool active) { fIsActiveWin = active; fHideCursorCountDown = HIDE_CURSOR_DELAY_TIME; }