diff options
author | Cow <cow@volloeko.de> | 2020-07-05 05:42:45 +0200 |
---|---|---|
committer | Cow <cow@volloeko.de> | 2020-07-05 05:42:45 +0200 |
commit | 1b446698398c648b38311975a6cfd54859ea5cfe (patch) | |
tree | 521ecc4ce9ad968281094eb8c5453dca606931e3 /src/main/java/eu/olli/cowlection/search/GuiSearch.java | |
parent | edaca1fd41a612c71c526ceb20b89c5dec2d81b3 (diff) | |
download | Cowlection-1b446698398c648b38311975a6cfd54859ea5cfe.tar.gz Cowlection-1b446698398c648b38311975a6cfd54859ea5cfe.tar.bz2 Cowlection-1b446698398c648b38311975a6cfd54859ea5cfe.zip |
Renamed mod to Cowlection
Bumped version to 1.8.9-0.7.0
Diffstat (limited to 'src/main/java/eu/olli/cowlection/search/GuiSearch.java')
-rw-r--r-- | src/main/java/eu/olli/cowlection/search/GuiSearch.java | 603 |
1 files changed, 603 insertions, 0 deletions
diff --git a/src/main/java/eu/olli/cowlection/search/GuiSearch.java b/src/main/java/eu/olli/cowlection/search/GuiSearch.java new file mode 100644 index 0000000..d693e59 --- /dev/null +++ b/src/main/java/eu/olli/cowlection/search/GuiSearch.java @@ -0,0 +1,603 @@ +package eu.olli.cowlection.search; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.mojang.realmsclient.util.Pair; +import eu.olli.cowlection.Cowlection; +import eu.olli.cowlection.config.MooConfig; +import eu.olli.cowlection.data.LogEntry; +import eu.olli.cowlection.util.Utils; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.*; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.util.ChatComponentText; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.IChatComponent; +import net.minecraftforge.common.ForgeVersion; +import net.minecraftforge.fml.client.GuiScrollingList; +import net.minecraftforge.fml.client.config.GuiButtonExt; +import net.minecraftforge.fml.client.config.GuiCheckBox; +import net.minecraftforge.fml.client.config.GuiUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.commons.lang3.tuple.ImmutableTriple; +import org.lwjgl.input.Keyboard; +import org.lwjgl.opengl.GL11; + +import java.awt.*; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.*; +import java.util.concurrent.*; +import java.util.stream.Collectors; +import java.util.zip.GZIPInputStream; + +public class GuiSearch extends GuiScreen { + private static final String SEARCH_QUERY_PLACE_HOLDER = "Search for..."; + private final File mcLogOutputFile; + /** + * @see Executors#newCachedThreadPool() + */ + private final ExecutorService executorService = new ThreadPoolExecutor(0, 1, + 60L, TimeUnit.SECONDS, + new LinkedBlockingQueue<>(), new ThreadFactoryBuilder().setNameFormat(Cowlection.MODID + "-logfilesearcher-%d").build()); + // data + private String searchQuery; + private boolean chatOnly; + private boolean matchCase; + private boolean removeFormatting; + /** + * Cached results are required after resizing the client + */ + private List<LogEntry> searchResults; + private LocalDate dateStart; + private LocalDate dateEnd; + + // gui elements + private GuiButton buttonSearch; + private GuiButton buttonClose; + private GuiButton buttonHelp; + private GuiCheckBox checkboxChatOnly; + private GuiCheckBox checkboxMatchCase; + private GuiCheckBox checkboxRemoveFormatting; + private GuiTextField fieldSearchQuery; + private GuiDateField fieldDateStart; + private GuiDateField fieldDateEnd; + private SearchResults guiSearchResults; + private List<GuiTooltip> guiTooltips; + private boolean isSearchInProgress; + private String analyzedFiles; + private String analyzedFilesWithHits; + private boolean areEntriesSearchResults; + + public GuiSearch(File configDirectory) { + this.mcLogOutputFile = new File(configDirectory, "mc-log.txt"); + try { + mcLogOutputFile.createNewFile(); + } catch (IOException e) { + e.printStackTrace(); + } + this.searchQuery = SEARCH_QUERY_PLACE_HOLDER; + this.searchResults = new ArrayList<>(); + this.dateStart = MooConfig.calculateStartDate(); + this.dateEnd = LocalDate.now(); + this.chatOnly = true; + } + + /** + * Adds the buttons (and other controls) to the screen in question. Called when the GUI is displayed and when the + * window resizes, the buttonList is cleared beforehand. + */ + @Override + public void initGui() { + this.guiTooltips = new ArrayList<>(); + + this.fieldSearchQuery = new GuiTextField(42, this.fontRendererObj, this.width / 2 - 100, 13, 200, 20); + this.fieldSearchQuery.setMaxStringLength(255); + this.fieldSearchQuery.setText(searchQuery); + if (SEARCH_QUERY_PLACE_HOLDER.equals(searchQuery)) { + this.fieldSearchQuery.setFocused(true); + this.fieldSearchQuery.setSelectionPos(0); + } + + // date field: start + this.fieldDateStart = new GuiDateField(50, this.fontRendererObj, this.width / 2 + 110, 15, 70, 15); + this.fieldDateStart.setText(dateStart.toString()); + addTooltip(fieldDateStart, Arrays.asList(EnumChatFormatting.YELLOW + "Start date", "" + EnumChatFormatting.GRAY + EnumChatFormatting.ITALIC + "Format: " + EnumChatFormatting.RESET + "year-month-day")); + // date field: end + this.fieldDateEnd = new GuiDateField(51, this.fontRendererObj, this.width / 2 + 110, 35, 70, 15); + this.fieldDateEnd.setText(dateEnd.toString()); + addTooltip(fieldDateEnd, Arrays.asList(EnumChatFormatting.YELLOW + "End date", "" + EnumChatFormatting.GRAY + EnumChatFormatting.ITALIC + "Format: " + EnumChatFormatting.RESET + "year-month-day")); + + // close + this.buttonList.add(this.buttonClose = new GuiButtonExt(0, this.width - 25, 3, 22, 20, EnumChatFormatting.RED + "X")); + addTooltip(buttonClose, Arrays.asList(EnumChatFormatting.RED + "Close search interface", "" + EnumChatFormatting.GRAY + EnumChatFormatting.ITALIC + "Hint:" + EnumChatFormatting.RESET + " alternatively press ESC")); + // help + this.buttonList.add(this.buttonHelp = new GuiButtonExt(1, this.width - 25 - 25, 3, 22, 20, "?")); + addTooltip(buttonHelp, Collections.singletonList(EnumChatFormatting.YELLOW + "Show help")); + + // chatOnly + this.buttonList.add(this.checkboxChatOnly = new GuiCheckBox(21, this.width / 2 - 100, 35, " Chatbox only", chatOnly)); + addTooltip(checkboxChatOnly, Collections.singletonList(EnumChatFormatting.YELLOW + "Should " + EnumChatFormatting.GOLD + "only " + EnumChatFormatting.YELLOW + "results that have " + EnumChatFormatting.GOLD + "appeared in the chat box " + EnumChatFormatting.YELLOW + "be displayed?\n" + + EnumChatFormatting.GRAY + "For example, this " + EnumChatFormatting.WHITE + "excludes error messages" + EnumChatFormatting.GRAY + " but still " + EnumChatFormatting.WHITE + "includes messages sent by a server" + EnumChatFormatting.GRAY + ".")); + // matchCase + this.buttonList.add(this.checkboxMatchCase = new GuiCheckBox(20, this.width / 2 - 100, 45, " Match case", matchCase)); + addTooltip(checkboxMatchCase, Collections.singletonList(EnumChatFormatting.YELLOW + "Should the search be " + EnumChatFormatting.GOLD + "case-sensitive" + EnumChatFormatting.YELLOW + "?")); + // removeFormatting + this.buttonList.add(this.checkboxRemoveFormatting = new GuiCheckBox(22, this.width / 2 - 100, 55, " Remove formatting", removeFormatting)); + addTooltip(checkboxRemoveFormatting, Collections.singletonList(EnumChatFormatting.YELLOW + "Should " + EnumChatFormatting.GOLD + "formatting " + EnumChatFormatting.YELLOW + "and " + EnumChatFormatting.GOLD + "color codes " + EnumChatFormatting.YELLOW + "be " + EnumChatFormatting.GOLD + "removed " + EnumChatFormatting.YELLOW + "from the search results?")); + // search + this.buttonList.add(this.buttonSearch = new GuiButtonExt(100, this.width / 2 + 40, 40, 60, 20, "Search")); + + this.guiSearchResults = new SearchResults(70); + this.guiSearchResults.setResults(searchResults); + + this.setIsSearchInProgress(isSearchInProgress); + + boolean isStartDateValid = fieldDateStart.validateDate(); + boolean isEndDateValid = fieldDateEnd.validateDate(); + this.buttonSearch.enabled = !isSearchInProgress && this.fieldSearchQuery.getText().trim().length() > 1 && !this.fieldSearchQuery.getText().startsWith(SEARCH_QUERY_PLACE_HOLDER) && isStartDateValid && isEndDateValid && !dateStart.isAfter(dateEnd); + + if (isStartDateValid && isEndDateValid && dateStart.isAfter(dateEnd)) { + fieldDateStart.setTextColor(0xFFDD3333); + fieldDateEnd.setTextColor(0xFFCC3333); + } + } + + private <T extends Gui> void addTooltip(T field, List<String> tooltip) { + GuiTooltip guiTooltip = new GuiTooltip(field, tooltip); + this.guiTooltips.add(guiTooltip); + } + + @Override + public void updateScreen() { + fieldSearchQuery.updateCursorCounter(); + fieldDateStart.updateCursorCounter(); + fieldDateEnd.updateCursorCounter(); + } + + @Override + protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException { + // allow clicks on 'close' button even while a search is in progress + super.mouseClicked(mouseX, mouseY, mouseButton); + if (isSearchInProgress) { + // search in progress, abort + return; + } + fieldSearchQuery.mouseClicked(mouseX, mouseY, mouseButton); + fieldDateStart.mouseClicked(mouseX, mouseY, mouseButton); + fieldDateEnd.mouseClicked(mouseX, mouseY, mouseButton); + } + + @Override + protected void keyTyped(char typedChar, int keyCode) throws IOException { + if (isSearchInProgress && keyCode != Keyboard.KEY_ESCAPE) { + // search in progress, don't process key typed - but allow escape to exit gui + return; + } + if (dateStart.isBefore(dateEnd)) { + fieldDateStart.setTextColor(0xFFFFFFFF); + fieldDateEnd.setTextColor(0xFFFFFFFF); + } + if (keyCode == Keyboard.KEY_RETURN && this.fieldSearchQuery.isFocused()) { + // perform search + actionPerformed(buttonSearch); + } else if (this.fieldSearchQuery.textboxKeyTyped(typedChar, keyCode)) { + searchQuery = this.fieldSearchQuery.getText(); + } else if (this.fieldDateStart.textboxKeyTyped(typedChar, keyCode)) { + if (fieldDateStart.validateDate()) { + dateStart = fieldDateStart.getDate(); + } + } else if (this.fieldDateEnd.textboxKeyTyped(typedChar, keyCode)) { + if (fieldDateEnd.validateDate()) { + dateEnd = fieldDateEnd.getDate(); + } + } else if (GuiScreen.isKeyComboCtrlA(keyCode)) { + // copy all search results + String searchResults = guiSearchResults.getAllSearchResults(); + if (!searchResults.isEmpty()) { + GuiScreen.setClipboardString(searchResults); + } + } else if (GuiScreen.isKeyComboCtrlC(keyCode)) { + // copy current selected entry + LogEntry selectedSearchResult = guiSearchResults.getSelectedSearchResult(); + if (selectedSearchResult != null) { + GuiScreen.setClipboardString(EnumChatFormatting.getTextWithoutFormattingCodes(selectedSearchResult.getMessage())); + } + } else if (keyCode == Keyboard.KEY_C && isCtrlKeyDown() && isShiftKeyDown() && !isAltKeyDown()) { + // copy current selected entry with formatting codes + LogEntry selectedSearchResult = guiSearchResults.getSelectedSearchResult(); + if (selectedSearchResult != null) { + GuiScreen.setClipboardString(selectedSearchResult.getMessage()); + } + } else { + if (keyCode == Keyboard.KEY_ESCAPE) { + guiSearchResults = null; + } + super.keyTyped(typedChar, keyCode); + } + + boolean isStartDateValid = fieldDateStart.validateDate(); + boolean isEndDateValid = fieldDateEnd.validateDate(); + this.buttonSearch.enabled = !isSearchInProgress && searchQuery.trim().length() > 1 && !searchQuery.startsWith(SEARCH_QUERY_PLACE_HOLDER) && isStartDateValid && isEndDateValid && !dateStart.isAfter(dateEnd); + + if (isStartDateValid && isEndDateValid && dateStart.isAfter(dateEnd)) { + fieldDateStart.setTextColor(0xFFDD3333); + fieldDateEnd.setTextColor(0xFFCC3333); + } + } + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + this.drawDefaultBackground(); + this.drawCenteredString(this.fontRendererObj, EnumChatFormatting.BOLD + "Minecraft Log Search", this.width / 2, 2, 0xFFFFFF); + this.fieldSearchQuery.drawTextBox(); + this.fieldDateStart.drawTextBox(); + this.fieldDateEnd.drawTextBox(); + this.guiSearchResults.drawScreen(mouseX, mouseY, partialTicks); + + super.drawScreen(mouseX, mouseY, partialTicks); + + for (GuiTooltip guiTooltip : guiTooltips) { + if (guiTooltip.checkHover(mouseX, mouseY)) { + drawHoveringText(guiTooltip.getText(), mouseX, mouseY, 300); + // only one tooltip can be displayed at a time: break! + break; + } + } + } + + @Override + protected void actionPerformed(GuiButton button) throws IOException { + if (button == this.buttonClose && button.enabled) { + guiSearchResults = null; + this.mc.setIngameFocus(); + } + if (isSearchInProgress || !button.enabled) { + return; + } + if (button == this.buttonSearch) { + setIsSearchInProgress(true); + + executorService.execute(() -> { + try { + ImmutableTriple<Integer, Integer, List<LogEntry>> searchResultsData = new LogFilesSearcher().searchFor(this.fieldSearchQuery.getText(), checkboxChatOnly.isChecked(), checkboxMatchCase.isChecked(), checkboxRemoveFormatting.isChecked(), dateStart, dateEnd); + this.searchResults = searchResultsData.right; + this.analyzedFiles = "Analyzed files: " + EnumChatFormatting.WHITE + searchResultsData.left; + this.analyzedFilesWithHits = "Files with hits: " + EnumChatFormatting.WHITE + searchResultsData.middle; + if (this.searchResults.isEmpty()) { + this.searchResults.add(new LogEntry(EnumChatFormatting.ITALIC + "No results")); + areEntriesSearchResults = false; + } else { + areEntriesSearchResults = true; + } + } catch (IOException e) { + System.err.println("Error reading/parsing file log files:"); + e.printStackTrace(); + if (e.getStackTrace().length > 0) { + searchResults.add(new LogEntry(StringUtils.replaceEach(ExceptionUtils.getStackTrace(e), new String[]{"\t", "\r\n"}, new String[]{" ", "\n"}))); + } + } + Minecraft.getMinecraft().addScheduledTask(() -> { + this.guiSearchResults.setResults(this.searchResults); + setIsSearchInProgress(false); + }); + }); + } else if (button == checkboxChatOnly) { + chatOnly = checkboxChatOnly.isChecked(); + } else if (button == checkboxMatchCase) { + matchCase = checkboxMatchCase.isChecked(); + } else if (button == checkboxRemoveFormatting) { + removeFormatting = checkboxRemoveFormatting.isChecked(); + } else if (button == buttonHelp) { + this.areEntriesSearchResults = false; + this.searchResults.clear(); + this.searchResults.add(new LogEntry("" + EnumChatFormatting.GOLD + EnumChatFormatting.BOLD + "Initial setup/Configuration " + EnumChatFormatting.GRAY + EnumChatFormatting.ITALIC + "/moo config")); + this.searchResults.add(new LogEntry(EnumChatFormatting.GOLD + " 1) " + EnumChatFormatting.RESET + "Configure directories that should be scanned for log files (\"Directories with Minecraft log files\")")); + this.searchResults.add(new LogEntry(EnumChatFormatting.GOLD + " 2) " + EnumChatFormatting.RESET + "Set default starting date (\"Start date for log file search\")")); + this.searchResults.add(new LogEntry("" + EnumChatFormatting.GOLD + EnumChatFormatting.BOLD + "Performing a search " + EnumChatFormatting.GRAY + EnumChatFormatting.ITALIC + "/moo search")); + this.searchResults.add(new LogEntry(EnumChatFormatting.GOLD + " 1) " + EnumChatFormatting.RESET + "Enter search term")); + this.searchResults.add(new LogEntry(EnumChatFormatting.GOLD + " 2) " + EnumChatFormatting.RESET + "Adjust start and end date")); + this.searchResults.add(new LogEntry(EnumChatFormatting.GOLD + " 3) " + EnumChatFormatting.RESET + "Select desired options (match case, ...)")); + this.searchResults.add(new LogEntry(EnumChatFormatting.GOLD + " 4) " + EnumChatFormatting.RESET + "Click 'Search'")); + this.searchResults.add(new LogEntry("" + EnumChatFormatting.GOLD + EnumChatFormatting.BOLD + "Search results")); + this.searchResults.add(new LogEntry(EnumChatFormatting.GOLD + " - " + EnumChatFormatting.YELLOW + "CTRL + C " + EnumChatFormatting.RESET + "to copy selected search result")); + this.searchResults.add(new LogEntry(EnumChatFormatting.GOLD + " - " + EnumChatFormatting.YELLOW + "CTRL + Shift + C " + EnumChatFormatting.RESET + "to copy selected search result " + EnumChatFormatting.ITALIC + "with" + EnumChatFormatting.RESET + " formatting codes")); + this.searchResults.add(new LogEntry(EnumChatFormatting.GOLD + " - " + EnumChatFormatting.YELLOW + "CTRL + A " + EnumChatFormatting.RESET + "to copy all search results")); + this.searchResults.add(new LogEntry(EnumChatFormatting.GOLD + " - " + EnumChatFormatting.YELLOW + "Double click search result " + EnumChatFormatting.RESET + "to open corresponding log file in default text editor")); + this.guiSearchResults.setResults(searchResults); + } + } + + private void setIsSearchInProgress(boolean isSearchInProgress) { + this.isSearchInProgress = isSearchInProgress; + buttonSearch.enabled = !isSearchInProgress; + fieldSearchQuery.setEnabled(!isSearchInProgress); + fieldDateStart.setEnabled(!isSearchInProgress); + fieldDateEnd.setEnabled(!isSearchInProgress); + checkboxChatOnly.enabled = !isSearchInProgress; + checkboxMatchCase.enabled = !isSearchInProgress; + checkboxRemoveFormatting.enabled = !isSearchInProgress; + if (isSearchInProgress) { + fieldSearchQuery.setFocused(false); + fieldDateStart.setFocused(false); + fieldDateEnd.setFocused(false); + buttonSearch.displayString = EnumChatFormatting.ITALIC + "Searching"; + searchResults.clear(); + guiSearchResults.clearResults(); + analyzedFiles = null; + analyzedFilesWithHits = null; + } else { + buttonSearch.displayString = "Search"; + } + } + + private void drawHoveringText(List<String> textLines, int mouseX, int mouseY, int maxTextWidth) { + if (ForgeVersion.getBuildVersion() < 1808) { + // we're running a forge version from before 24 March 2016 (http://files.minecraftforge.net/maven/net/minecraftforge/forge/index_1.8.9.html for reference) + // using mc built-in method + drawHoveringText(textLines, mouseX, mouseY, fontRendererObj); + } else { + // we're on a newer forge version, so we can use the improved tooltip rendering added in 1.8.9-11.15.1.1808 (released 03/24/16 09:25 PM) in this pull request: https://github.com/MinecraftForge/MinecraftForge/pull/2649 + GuiUtils.drawHoveringText(textLines, mouseX, mouseY, width, height, maxTextWidth, fontRendererObj); + } + } + + /** + * List gui element similar to GuiModList.Info + */ + class SearchResults extends GuiScrollingList { + private final String[] spinner = new String[]{"oooooo", "Oooooo", "oOoooo", "ooOooo", "oooOoo", "ooooOo", "oooooO"}; + private final DateTimeFormatter coloredDateFormatter = DateTimeFormatter.ofPattern(EnumChatFormatting.GRAY + "HH" + EnumChatFormatting.DARK_GRAY + ":" + EnumChatFormatting.GRAY + "mm" + EnumChatFormatting.DARK_GRAY + ":" + EnumChatFormatting.GRAY + "ss"); + private List<LogEntry> rawResults; + private List<IChatComponent> slotsData; + /** + * key: slot id of 1st line of a search result (if multi-line-result), value: search result id + */ + private NavigableMap<Integer, Integer> searchResultEntries; + private Pair<Long, String> errorMessage; + private String resultsCount; + + SearchResults(int marginTop) { + super(GuiSearch.this.mc, + GuiSearch.this.width - 10, // 5 pixel margin each + GuiSearch.this.height - marginTop - 5, + marginTop, GuiSearch.this.height - 5, + 5, 12, + GuiSearch.this.width, + GuiSearch.this.height); + this.rawResults = Collections.emptyList(); + this.slotsData = Collections.emptyList(); + this.searchResultEntries = Collections.emptyNavigableMap(); + } + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + super.drawScreen(mouseX, mouseY, partialTicks); + if (isSearchInProgress) { + // spinner taken from IProgressMeter and GuiAchievements#drawScreen + GuiSearch.this.drawCenteredString(GuiSearch.this.fontRendererObj, "Searching for '" + GuiSearch.this.searchQuery + "'", GuiSearch.this.width / 2, GuiSearch.this.height / 2, 16777215); + GuiSearch.this.drawCenteredString(GuiSearch.this.fontRendererObj, spinner[(int) (Minecraft.getSystemTime() / 150L % (long) spinner.length)], GuiSearch.this.width / 2, GuiSearch.this.height / 2 + GuiSearch.this.fontRendererObj.FONT_HEIGHT * 2, 16777215); + } + int hoveredSlotId = this.func_27256_c(mouseX, mouseY); + if (hoveredSlotId >= 0 && mouseY > top && mouseY < bottom) { + float scrollDistance = getScrollDistance(); + if (scrollDistance != Float.MIN_VALUE) { + // draw hovered entry details + + int hoveredSearchResultId = getSearchResultIdBySlotId(hoveredSlotId); + LogEntry hoveredEntry = getSearchResultByResultId(hoveredSearchResultId); + if (hoveredEntry != null && !hoveredEntry.isError()) { + // draw 'tooltips' in the top left corner + drawString(fontRendererObj, "Log file: ", 2, 2, 0xff888888); + GlStateManager.pushMatrix(); + float scaleFactor = 0.75f; + GL11.glScalef(scaleFactor, scaleFactor, scaleFactor); + fontRendererObj.drawSplitString(EnumChatFormatting.GRAY + Utils.toRealPath(hoveredEntry.getFilePath()), 5, (int) ((4 + fontRendererObj.FONT_HEIGHT) * (1 / scaleFactor)), (int) ((GuiSearch.this.fieldSearchQuery.xPosition - 8) * (1 / scaleFactor)), 0xff888888); + GlStateManager.popMatrix(); + drawString(fontRendererObj, "Result: " + EnumChatFormatting.WHITE + (hoveredSearchResultId + 1) + EnumChatFormatting.RESET + "/" + EnumChatFormatting.WHITE + this.rawResults.size(), 8, 48, 0xff888888); + drawString(fontRendererObj, "Time: " + hoveredEntry.getTime().format(coloredDateFormatter), 8, 58, 0xff888888); + } + + // formula from GuiScrollingList#drawScreen slotTop + int baseY = this.top + /* border: */4 - (int) scrollDistance; + + // highlight multiline search results + Integer resultIndexStart = searchResultEntries.floorKey(hoveredSlotId); + Integer resultIndexEnd = searchResultEntries.higherKey(hoveredSlotId); + + if (resultIndexStart == null) { + return; + } else if (resultIndexEnd == null) { + // last result entry + resultIndexEnd = getSize(); + } + + int slotTop = baseY + resultIndexStart * this.slotHeight - 2; + int slotBottom = baseY + resultIndexEnd * this.slotHeight - 2; + drawRect(this.left, Math.max(slotTop, top), right - /* scrollBar: */7, Math.min(slotBottom, bottom), 0x22ffffff); + } + } else if (areEntriesSearchResults) { + if (analyzedFiles != null) { + drawString(fontRendererObj, analyzedFiles, 8, 22, 0xff888888); + } + if (analyzedFilesWithHits != null) { + drawString(fontRendererObj, analyzedFilesWithHits, 8, 32, 0xff888888); + } + if (resultsCount != null) { + drawString(fontRendererObj, resultsCount, 8, 48, 0xff888888); + } + } + if (errorMessage != null) { + if (errorMessage.first().compareTo(System.currentTimeMillis()) > 0) { + String errorText = "Error: " + EnumChatFormatting.RED + errorMessage.second(); + int stringWidth = fontRendererObj.getStringWidth(errorText); + int margin = 5; + int left = width / 2 - stringWidth / 2 - margin; + int top = height / 2 - margin; + drawRect(left, top, left + stringWidth + 2 * margin, top + fontRendererObj.FONT_HEIGHT + 2 * margin, 0xff000000); + drawCenteredString(fontRendererObj, errorText,/* 2, 30*/width / 2, height / 2, 0xffDD1111); + } else { + errorMessage = null; + } + } + } + + private float getScrollDistance() { + Field scrollDistanceField = FieldUtils.getField(GuiScrollingList.class, "scrollDistance", true); + if (scrollDistanceField == null) { + // scrollDistance field not found in class GuiScrollingList + return Float.MIN_VALUE; + } + try { + return (float) scrollDistanceField.get(this); + } catch (IllegalAccessException e) { + e.printStackTrace(); + return Float.MIN_VALUE; + } + } + + @Override + protected int getSize() { + return slotsData.size(); + } + + @Override + protected void elementClicked(int index, boolean doubleClick) { + if (doubleClick) { + int searchResultIdBySlotId = getSearchResultIdBySlotId(index); + LogEntry searchResult = rawResults.get(searchResultIdBySlotId); + if (searchResult.getFilePath() == null) { + setErrorMessage("This log entry is not from a file"); + return; + } + byte[] buffer = new byte[1024]; + String logFileName = Utils.toRealPath(searchResult.getFilePath()); + if (logFileName.endsWith("latest.log")) { + try { + Files.copy(searchResult.getFilePath(), mcLogOutputFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + e.printStackTrace(); + } + } else { // .log.gz + String newLine = System.getProperty("line.separator"); + String fileHeader = "# Original filename: " + logFileName + newLine + "# Use CTRL + F to search for specific words" + newLine + newLine; + try (GZIPInputStream logFileGzipped = new GZIPInputStream(new FileInputStream(logFileName)); + FileOutputStream logFileUnGzipped = new FileOutputStream(mcLogOutputFile)) { + logFileUnGzipped.write(fileHeader.getBytes()); + int len; + while ((len = logFileGzipped.read(buffer)) > 0) { + logFileUnGzipped.write(buffer, 0, len); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + try { + Desktop.getDesktop().open(mcLogOutputFile); + } catch (IOException e) { + setErrorMessage("File extension .txt has no associated default editor"); + e.printStackTrace(); + } catch (IllegalArgumentException e) { + setErrorMessage(e.getMessage()); // The file: <path> doesn't exist. + e.printStackTrace(); + } catch (UnsupportedOperationException e) { + setErrorMessage("Can't open files on this OS"); + e.printStackTrace(); + } + } + } + + private void setErrorMessage(String errorMessage) { + int showDuration = 10000; // ms + this.errorMessage = Pair.of(System.currentTimeMillis() + showDuration, errorMessage); + } + + @Override + protected boolean isSelected(int index) { + return false; + } + + @Override + protected void drawBackground() { + } + + @Override + protected void drawSlot(int slotIdx, int entryRight, int slotTop, int slotBuffer, Tessellator tess) { + int drawnResultIndex = searchResultEntries.floorKey(slotIdx); + if (Objects.equals(searchResultEntries.floorKey(selectedIndex), drawnResultIndex)) { + // highlight all lines of selected entry + drawRect(this.left, slotTop - 2, entryRight, slotTop + slotHeight - 2, 0x99000000); + } + IChatComponent slotData = slotsData.get(slotIdx); + if (slotData != null) { + GlStateManager.enableBlend(); + GuiSearch.this.fontRendererObj.drawStringWithShadow(slotData.getFormattedText(), this.left + 4, slotTop, 0xFFFFFF); + GlStateManager.disableAlpha(); + GlStateManager.disableBlend(); + } + } + + private void setResults(List<LogEntry> searchResult) { + this.rawResults = searchResult; + this.slotsData = resizeContent(searchResult); + if (GuiSearch.this.areEntriesSearchResults) { + this.resultsCount = "Results: " + EnumChatFormatting.WHITE + this.rawResults.size(); + } + } + + private void clearResults() { + this.rawResults = Collections.emptyList(); + this.resultsCount = null; + this.slotsData = resizeContent(Collections.emptyList()); + } + + private List<IChatComponent> resizeContent(List<LogEntry> searchResults) { + this.searchResultEntries = new TreeMap<>(); + List<IChatComponent> slotsData = new ArrayList<>(); + for (int searchResultIndex = 0; searchResultIndex < searchResults.size(); searchResultIndex++) { + LogEntry searchResult = searchResults.get(searchResultIndex); + + String searchResultEntry; + if (searchResult.isError()) { + searchResultEntry = searchResult.getMessage(); + } else { + searchResultEntry = EnumChatFormatting.DARK_GRAY + searchResult.getTime().format(DateTimeFormatter.ISO_LOCAL_DATE) + " " + EnumChatFormatting.RESET + searchResult.getMessage(); + } + searchResultEntries.put(slotsData.size(), searchResultIndex); + List<IChatComponent> multilineResult = GuiUtilRenderComponents.splitText(new ChatComponentText(searchResultEntry), this.listWidth - 8, GuiSearch.this.fontRendererObj, false, true); + slotsData.addAll(multilineResult); + } + return slotsData; + } + + LogEntry getSelectedSearchResult() { + int searchResultId = getSearchResultIdBySlotId(selectedIndex); + return getSearchResultByResultId(searchResultId); + } + + private LogEntry getSearchResultByResultId(int searchResultId) { + return (searchResultId >= 0 && searchResultId < rawResults.size()) ? rawResults.get(searchResultId) : null; + } + + private int getSearchResultIdBySlotId(int slotId) { + Map.Entry<Integer, Integer> searchResultIds = searchResultEntries.floorEntry(slotId); + return searchResultIds != null ? searchResultIds.getValue() : -1; + } + + String getAllSearchResults() { + return rawResults.stream().map(logEntry -> EnumChatFormatting.getTextWithoutFormattingCodes(logEntry.getMessage())) + .collect(Collectors.joining("\n")); + } + } +} |