/* * Autopsy Forensic Browser * * Copyright 2013-16 Basis Technology Corp. * Contact: carrier <at> sleuthkit <dot> org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.sleuthkit.autopsy.imagegallery.gui.drawableviews; import com.google.common.eventbus.Subscribe; import java.util.ArrayList; import java.util.Collection; import java.util.Optional; import java.util.logging.Level; import javafx.application.Platform; import javafx.beans.InvalidationListener; import javafx.beans.WeakInvalidationListener; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.control.MenuItem; import javafx.scene.control.ToggleButton; import javafx.scene.control.Tooltip; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; import javafx.scene.layout.Border; import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderStroke; import javafx.scene.layout.BorderStrokeStyle; import javafx.scene.layout.BorderWidths; import javafx.scene.layout.CornerRadii; import javafx.scene.layout.Region; import javafx.scene.paint.Color; import javax.swing.Action; import javax.swing.SwingUtilities; import org.controlsfx.control.action.ActionUtils; import org.openide.util.Lookup; import org.openide.util.NbBundle; import org.openide.util.actions.Presenter; import org.openide.windows.TopComponent; import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.corecomponentinterfaces.ContextMenuActionsProvider; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.datamodel.FileNode; import org.sleuthkit.autopsy.directorytree.ExtractAction; import org.sleuthkit.autopsy.directorytree.NewWindowViewAction; import org.sleuthkit.autopsy.imagegallery.FileIDSelectionModel; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.ImageGalleryTopComponent; import org.sleuthkit.autopsy.imagegallery.actions.AddTagAction; import org.sleuthkit.autopsy.imagegallery.actions.CategorizeAction; import org.sleuthkit.autopsy.imagegallery.actions.DeleteFollowUpTagAction; import org.sleuthkit.autopsy.imagegallery.actions.OpenExternalViewerAction; import org.sleuthkit.autopsy.imagegallery.actions.SwingMenuItemAdapter; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile; import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.TagName; import org.sleuthkit.datamodel.TskCoreException; /** * An abstract base class for {@link DrawableTile} and {@link SlideShowView}, * since they share a similar node tree and many behaviors, other implementors * of {@link DrawableView}s should implement the interface directly * * * TODO: refactor ExternalViewerAction to supply its own name */ @NbBundle.Messages({"DrawableTileBase.externalViewerAction.text=Open in External Viewer"}) public abstract class DrawableTileBase extends DrawableUIBase { private static final Logger LOGGER = Logger.getLogger(DrawableTileBase.class.getName()); private static final Border UNSELECTED_BORDER = new Border(new BorderStroke(Color.GRAY, BorderStrokeStyle.SOLID, new CornerRadii(2), new BorderWidths(3))); private static final Border SELECTED_BORDER = new Border(new BorderStroke(Color.BLUE, BorderStrokeStyle.SOLID, new CornerRadii(2), new BorderWidths(3))); //TODO: do this in CSS? -jm protected static final Image followUpIcon = new Image("org/sleuthkit/autopsy/imagegallery/images/flag_red.png"); //NON-NLS protected static final Image followUpGray = new Image("org/sleuthkit/autopsy/imagegallery/images/flag_gray.png"); //NON-NLS protected final FileIDSelectionModel selectionModel; private static ContextMenu contextMenu; /** * displays the icon representing video files */ @FXML private ImageView fileTypeImageView; /** * displays the icon representing hash hits */ @FXML private ImageView hashHitImageView; /** * displays the icon representing follow up tag */ @FXML private ImageView followUpImageView; @FXML private ToggleButton followUpToggle; @FXML BorderPane imageBorder; /** * the label that shows the name of the represented file */ @FXML Label nameLabel; @FXML protected ImageView imageView; /** * the groupPane this {@link DrawableTileBase} is embedded in */ final private GroupPane groupPane; volatile private boolean registered = false; /** * * @param groupPane the value of groupPane * @param controller the value of controller */ @NbBundle.Messages({"DrawableTileBase.menuItem.extractFiles=Extract File(s)", "DrawableTileBase.menuItem.showContentViewer=Show Content Viewer"}) protected DrawableTileBase(GroupPane groupPane, final ImageGalleryController controller) { super(controller); this.groupPane = groupPane; selectionModel = controller.getSelectionModel(); selectionModel.getSelected().addListener(new WeakInvalidationListener(selectionListener)); //set up mouse listener addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() { @Override public void handle(MouseEvent t) { getFile().ifPresent(file -> { final long fileID = file.getId(); switch (t.getButton()) { case SECONDARY: if (t.getClickCount() == 1) { if (selectionModel.isSelected(fileID) == false) { groupPane.makeSelection(false, fileID); } } if (contextMenu != null) { contextMenu.hide(); } final ContextMenu groupContextMenu = groupPane.getContextMenu(); if (groupContextMenu != null) { groupContextMenu.hide(); } contextMenu = buildContextMenu(file); contextMenu.show(DrawableTileBase.this, t.getScreenX(), t.getScreenY()); break; } }); t.consume(); } private ContextMenu buildContextMenu(DrawableFile file) { final ArrayList<MenuItem> menuItems = new ArrayList<>(); menuItems.add(CategorizeAction.getCategoriesMenu(getController())); menuItems.add(AddTagAction.getTagMenu(getController())); final MenuItem extractMenuItem = new MenuItem(Bundle.DrawableTileBase_menuItem_extractFiles()); extractMenuItem.setOnAction(actionEvent -> { SwingUtilities.invokeLater(() -> { TopComponent etc = WindowManager.getDefault().findTopComponent(ImageGalleryTopComponent.PREFERRED_ID); ExtractAction.getInstance().actionPerformed(new java.awt.event.ActionEvent(etc, 0, null)); }); }); menuItems.add(extractMenuItem); MenuItem contentViewer = new MenuItem(Bundle.DrawableTileBase_menuItem_showContentViewer()); contentViewer.setOnAction(actionEvent -> { SwingUtilities.invokeLater(() -> { new NewWindowViewAction(Bundle.DrawableTileBase_menuItem_showContentViewer(), new FileNode(file.getAbstractFile())).actionPerformed(null); }); }); menuItems.add(contentViewer); OpenExternalViewerAction openExternalViewerAction = new OpenExternalViewerAction(file); MenuItem externalViewer = ActionUtils.createMenuItem(openExternalViewerAction); externalViewer.textProperty().unbind(); externalViewer.textProperty().bind(openExternalViewerAction.longTextProperty()); menuItems.add(externalViewer); Collection<? extends ContextMenuActionsProvider> menuProviders = Lookup.getDefault().lookupAll(ContextMenuActionsProvider.class); for (ContextMenuActionsProvider provider : menuProviders) { for (final Action act : provider.getActions()) { if (act instanceof Presenter.Popup) { Presenter.Popup aact = (Presenter.Popup) act; menuItems.add(SwingMenuItemAdapter.create(aact.getPopupPresenter())); } } } ContextMenu contextMenu = new ContextMenu(menuItems.toArray(new MenuItem[]{})); contextMenu.setAutoHide(true); return contextMenu; } }); } private final InvalidationListener selectionListener = observable -> updateSelectionState(); GroupPane getGroupPane() { return groupPane; } protected abstract String getTextForLabel(); protected void initialize() { followUpToggle.setOnAction(actionEvent -> { getFile().ifPresent(file -> { if (followUpToggle.isSelected() == true) { try { selectionModel.clearAndSelect(file.getId()); new AddTagAction(getController(), getController().getTagsManager().getFollowUpTagName(), selectionModel.getSelected()).handle(actionEvent); } catch (TskCoreException ex) { LOGGER.log(Level.SEVERE, "Failed to add Follow Up tag. Could not load TagName.", ex); //NON-NLS } } else { new DeleteFollowUpTagAction(getController(), file).handle(actionEvent); } }); }); } protected boolean hasFollowUp() { if (getFileID().isPresent()) { try { TagName followUpTagName = getController().getTagsManager().getFollowUpTagName(); return DrawableAttribute.TAGS.getValue(getFile().get()).stream() .anyMatch(followUpTagName::equals); } catch (TskCoreException ex) { LOGGER.log(Level.WARNING, "failed to get follow up tag name ", ex); //NON-NLS return true; } } else { return false; } } @Override synchronized protected void setFileHelper(final Long newFileID) { setFileIDOpt(Optional.ofNullable(newFileID)); setFileOpt(Optional.empty()); disposeContent(); if (getFileID().isPresent() == false || Case.isCaseOpen() == false) { if (registered == true) { getController().getCategoryManager().unregisterListener(this); getController().getTagsManager().unregisterListener(this); registered = false; } updateContent(); } else { if (registered == false) { getController().getCategoryManager().registerListener(this); getController().getTagsManager().registerListener(this); registered = true; } updateSelectionState(); updateCategory(); updateFollowUpIcon(); updateContent(); updateMetaData(); } } private void updateMetaData() { getFile().ifPresent(file -> { final boolean isVideo = file.isVideo(); final boolean hasHashSetHits = hasHashHit(); final String text = getTextForLabel(); Platform.runLater(() -> { fileTypeImageView.setManaged(isVideo); fileTypeImageView.setVisible(isVideo); hashHitImageView.setManaged(hasHashSetHits); hashHitImageView.setVisible(hasHashSetHits); nameLabel.setText(text); nameLabel.setTooltip(new Tooltip(text)); }); }); } /** * update the visual representation of the selection state of this * DrawableView */ protected void updateSelectionState() { getFileID().ifPresent(fileID -> { final boolean selected = selectionModel.isSelected(fileID); Platform.runLater(() -> setBorder(selected ? SELECTED_BORDER : UNSELECTED_BORDER)); }); } @Override public Region getCategoryBorderRegion() { return imageBorder; } @Subscribe @Override public void handleTagAdded(ContentTagAddedEvent evt) { getFileID().ifPresent(fileID -> { try { final TagName followUpTagName = getController().getTagsManager().getFollowUpTagName(); final ContentTag addedTag = evt.getAddedTag(); if (fileID == addedTag.getContent().getId() && addedTag.getName().equals(followUpTagName)) { Platform.runLater(() -> { followUpImageView.setImage(followUpIcon); followUpToggle.setSelected(true); }); } } catch (TskCoreException ex) { LOGGER.log(Level.SEVERE, "Failed to get followup tag name. Unable to update follow up status for file. ", ex); //NON-NLS } }); } @Subscribe @Override public void handleTagDeleted(ContentTagDeletedEvent evt) { getFileID().ifPresent(fileID -> { try { final TagName followUpTagName = getController().getTagsManager().getFollowUpTagName(); final ContentTagDeletedEvent.DeletedContentTagInfo deletedTagInfo = evt.getDeletedTagInfo(); if (fileID == deletedTagInfo.getContentID() && deletedTagInfo.getName().equals(followUpTagName)) { updateFollowUpIcon(); } } catch (TskCoreException ex) { LOGGER.log(Level.SEVERE, "Failed to get followup tag name. Unable to update follow up status for file. ", ex); //NON-NLS } }); } private void updateFollowUpIcon() { boolean hasFollowUp = hasFollowUp(); Platform.runLater(() -> { followUpImageView.setImage(hasFollowUp ? followUpIcon : followUpGray); followUpToggle.setSelected(hasFollowUp); }); } }