diff --git a/src/main/java/nl/andrewlalis/blockbookbinder/control/BookExportActionListener.java b/src/main/java/nl/andrewlalis/blockbookbinder/control/BookExportActionListener.java index 2a2266f..8ff3858 100644 --- a/src/main/java/nl/andrewlalis/blockbookbinder/control/BookExportActionListener.java +++ b/src/main/java/nl/andrewlalis/blockbookbinder/control/BookExportActionListener.java @@ -5,6 +5,7 @@ import nl.andrewlalis.blockbookbinder.view.BookPreviewPanel; import org.jnativehook.GlobalScreen; import org.jnativehook.NativeHookException; +import javax.swing.*; import java.awt.*; import java.awt.datatransfer.Clipboard; import java.awt.event.ActionEvent; @@ -15,9 +16,11 @@ import java.util.logging.Logger; public class BookExportActionListener implements ActionListener { private final BookPreviewPanel bookPreviewPanel; private final Clipboard clipboard; + private final JButton cancelExportButton; - public BookExportActionListener(BookPreviewPanel bookPreviewPanel) { + public BookExportActionListener(BookPreviewPanel bookPreviewPanel, JButton cancelExportButton) { this.bookPreviewPanel = bookPreviewPanel; + this.cancelExportButton = cancelExportButton; this.clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); } @@ -25,7 +28,19 @@ public class BookExportActionListener implements ActionListener { public void actionPerformed(ActionEvent e) { System.out.println("Starting export."); final Book book = this.bookPreviewPanel.getBook(); - BookPagePasteListener pasteListener = new BookPagePasteListener(book, clipboard); + int choice = JOptionPane.showConfirmDialog( + this.bookPreviewPanel.getRootPane(), + "Press CTRL+V to initialize export.", + "Confirm Export", + JOptionPane.OK_CANCEL_OPTION + ); + if (choice == JOptionPane.CANCEL_OPTION) { + return; + } + this.cancelExportButton.setEnabled(true); + BookPagePasteListener pasteListener = new BookPagePasteListener(book, clipboard, this.bookPreviewPanel, this.cancelExportButton); + this.bookPreviewPanel.enableNavigation(false); + pasteListener.exportNextPage(); // Start by exporting the first page right away. try { // For catching native events, set logging here. Logger logger = Logger.getLogger(GlobalScreen.class.getPackage().getName()); diff --git a/src/main/java/nl/andrewlalis/blockbookbinder/control/BookPagePasteListener.java b/src/main/java/nl/andrewlalis/blockbookbinder/control/BookPagePasteListener.java index 8a2fcec..3f4c3a3 100644 --- a/src/main/java/nl/andrewlalis/blockbookbinder/control/BookPagePasteListener.java +++ b/src/main/java/nl/andrewlalis/blockbookbinder/control/BookPagePasteListener.java @@ -1,48 +1,98 @@ package nl.andrewlalis.blockbookbinder.control; import nl.andrewlalis.blockbookbinder.model.Book; +import nl.andrewlalis.blockbookbinder.view.BookPreviewPanel; import org.jnativehook.GlobalScreen; import org.jnativehook.NativeHookException; import org.jnativehook.keyboard.NativeKeyEvent; import org.jnativehook.keyboard.NativeKeyListener; +import javax.swing.*; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.StringSelection; +import java.awt.event.ActionListener; +/** + * Listener that listens for native key-presses that indicate the user has + * pasted something into a book. + */ public class BookPagePasteListener implements NativeKeyListener { private final Book book; private final Clipboard clipboard; + private final BookPreviewPanel bookPreviewPanel; + private final JButton cancelExportButton; + private final ActionListener cancelExportActionListener; private int nextPage; - public BookPagePasteListener(Book book, Clipboard clipboard) { + public BookPagePasteListener(Book book, Clipboard clipboard, BookPreviewPanel bookPreviewPanel, JButton cancelExportButton) { this.book = book; this.clipboard = clipboard; + this.bookPreviewPanel = bookPreviewPanel; + this.cancelExportButton = cancelExportButton; this.nextPage = 0; + this.cancelExportActionListener = (e) -> this.cancelExport(); + this.cancelExportButton.addActionListener(this.cancelExportActionListener); + } + + public void exportNextPage() { + // Sleep a little bit to avoid rapid repeats. + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + this.bookPreviewPanel.setCurrentPage(this.nextPage); + boolean clipboardSuccess = false; + while (!clipboardSuccess) { + try { + clipboard.setContents( + new StringSelection(book.getPages().get(this.nextPage).toString()), + null + ); + clipboardSuccess = true; + } catch (IllegalStateException e) { + System.err.println("Could not open and set contents of system clipboard."); + } + if (!clipboardSuccess) { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + System.out.println("Copied page " + this.nextPage + " into clipboard."); + this.nextPage++; + + // If we've reached the end of the book, unregister this listener and remove native hooks. + if (this.nextPage >= this.book.getPageCount()) { + this.cancelExport(); + } + } + + public void cancelExport() { + try { + this.bookPreviewPanel.enableNavigation(true); + this.cancelExportButton.setEnabled(false); + this.cancelExportButton.removeActionListener(this.cancelExportActionListener); + GlobalScreen.removeNativeKeyListener(this); + GlobalScreen.unregisterNativeHook(); + System.out.println("Done pasting."); + } catch (NativeHookException nativeHookException) { + System.err.println("Could not unregister a native hook."); + nativeHookException.printStackTrace(); + } } @Override - public void nativeKeyTyped(NativeKeyEvent nativeKeyEvent) {} + public void nativeKeyTyped(NativeKeyEvent nativeKeyEvent) { + } @Override public void nativeKeyPressed(NativeKeyEvent nativeKeyEvent) { if (nativeKeyEvent.getKeyCode() == NativeKeyEvent.VC_V && (nativeKeyEvent.getModifiers() & NativeKeyEvent.CTRL_MASK) > 0) { - System.out.println("CTRL + V -> Paste!!!"); - clipboard.setContents( - new StringSelection(book.getPages().get(this.nextPage).toString()), - null - ); - this.nextPage++; - System.out.println("Incremented page."); - if (this.nextPage >= this.book.getPageCount()) { - try { - GlobalScreen.removeNativeKeyListener(this); - GlobalScreen.unregisterNativeHook(); - System.out.println("Done pasting."); - } catch (NativeHookException nativeHookException) { - System.err.println("Could not unregister a native hook."); - nativeHookException.printStackTrace(); - } - } + this.exportNextPage(); } } diff --git a/src/main/java/nl/andrewlalis/blockbookbinder/control/ConvertToBookActionListener.java b/src/main/java/nl/andrewlalis/blockbookbinder/control/ConvertToBookActionListener.java index 90343aa..bff51d3 100644 --- a/src/main/java/nl/andrewlalis/blockbookbinder/control/ConvertToBookActionListener.java +++ b/src/main/java/nl/andrewlalis/blockbookbinder/control/ConvertToBookActionListener.java @@ -1,12 +1,16 @@ package nl.andrewlalis.blockbookbinder.control; -import nl.andrewlalis.blockbookbinder.model.BookBuilder; +import nl.andrewlalis.blockbookbinder.model.build.BookBuilder; import nl.andrewlalis.blockbookbinder.view.BookPreviewPanel; import nl.andrewlalis.blockbookbinder.view.SourceTextPanel; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +/** + * Action listener that, when activated, converts the text from the source panel + * into a formatted book. + */ public class ConvertToBookActionListener implements ActionListener { private final SourceTextPanel sourceTextPanel; private final BookPreviewPanel bookPreviewPanel; @@ -18,6 +22,8 @@ public class ConvertToBookActionListener implements ActionListener { @Override public void actionPerformed(ActionEvent e) { - this.bookPreviewPanel.setBook(new BookBuilder().build(this.sourceTextPanel.getSourceText())); + this.bookPreviewPanel.setBook( + new BookBuilder().build(this.sourceTextPanel.getSourceText()) + ); } } diff --git a/src/main/java/nl/andrewlalis/blockbookbinder/model/Book.java b/src/main/java/nl/andrewlalis/blockbookbinder/model/Book.java index 8d80596..d51a0d9 100644 --- a/src/main/java/nl/andrewlalis/blockbookbinder/model/Book.java +++ b/src/main/java/nl/andrewlalis/blockbookbinder/model/Book.java @@ -17,6 +17,10 @@ public class Book { return this.pages.size(); } + public void addPage(BookPage page) { + this.pages.add(page); + } + @Override public String toString() { StringBuilder sb = new StringBuilder("Book of " + this.getPageCount() + " pages:\n"); diff --git a/src/main/java/nl/andrewlalis/blockbookbinder/model/BookBuilder.java b/src/main/java/nl/andrewlalis/blockbookbinder/model/BookBuilder.java deleted file mode 100644 index 2cadc8f..0000000 --- a/src/main/java/nl/andrewlalis/blockbookbinder/model/BookBuilder.java +++ /dev/null @@ -1,73 +0,0 @@ -package nl.andrewlalis.blockbookbinder.model; - -import nl.andrewlalis.blockbookbinder.util.ApplicationProperties; - -/** - * Class which helps construct formatted book pages from a source text. - */ -public class BookBuilder { - public Book build(String source) { - Book book = new Book(); - char[] sourceChars = source.trim().toCharArray(); - - final int maxLines = ApplicationProperties.getIntProp("book.page_max_lines"); - final int maxLineWidth = ApplicationProperties.getIntProp("book.page_max_width"); - final CharWidthMapper charWidthMapper = new CharWidthMapper(); - - BookPage currentPage = new BookPage(); - StringBuilder lineStringBuilder = new StringBuilder(64); - int pageLineCount = 1; // Current line on the page we're on. - int lineCharWidth = 0; // Total pixel width of the current line so far. - int i = 0; - while (i < sourceChars.length) { - final char c = sourceChars[i]; - if (c == '\n') { - i++; - continue; - } - final int cWidth = charWidthMapper.getWidth(c); - boolean newLineNeeded = lineCharWidth + cWidth + 1 > maxLineWidth; - boolean newPageNeeded = pageLineCount == maxLines && newLineNeeded; - System.out.println("Current char: " + c + ", Current Line: " + pageLineCount + ", Current Line Char Width: " + lineCharWidth + ", New line needed: " + newLineNeeded + ", New page needed: " + newPageNeeded); - - // Check if the page is full, and append it to the book, and refresh. - if (newPageNeeded) { - // If necessary, append whatever is left in the last line to the page. - if (lineStringBuilder.length() > 0) { - currentPage.addLine(lineStringBuilder.toString()); - } - book.getPages().add(currentPage); - currentPage = new BookPage(); - // Reset all buffers and counters for the next page. - lineStringBuilder.setLength(0); - newLineNeeded = false; - pageLineCount = 1; - lineCharWidth = 0; - } - - // Check if the line is full, and append it to the page and refresh. - if (newLineNeeded) { - currentPage.addLine(lineStringBuilder.toString()); - // Reset line status info. - lineStringBuilder.setLength(0); - pageLineCount++; - lineCharWidth = 0; - } - - // Finally, append the char to the current line. - lineStringBuilder.append(c); - lineCharWidth += cWidth + 1; - i++; - } - - // Append a final page with the remainder of the text. - if (currentPage.hasContent()) { - if (lineStringBuilder.length() > 0) { - currentPage.addLine(lineStringBuilder.toString()); - } - book.getPages().add(currentPage); - } - - return book; - } -} diff --git a/src/main/java/nl/andrewlalis/blockbookbinder/model/build/BookBuilder.java b/src/main/java/nl/andrewlalis/blockbookbinder/model/build/BookBuilder.java new file mode 100644 index 0000000..20115eb --- /dev/null +++ b/src/main/java/nl/andrewlalis/blockbookbinder/model/build/BookBuilder.java @@ -0,0 +1,116 @@ +package nl.andrewlalis.blockbookbinder.model.build; + +import nl.andrewlalis.blockbookbinder.model.Book; +import nl.andrewlalis.blockbookbinder.model.BookPage; +import nl.andrewlalis.blockbookbinder.model.CharWidthMapper; +import nl.andrewlalis.blockbookbinder.util.ApplicationProperties; + +import java.util.ArrayList; +import java.util.List; + +public class BookBuilder { + private final CharWidthMapper charWidthMapper = new CharWidthMapper(); + + /** + * Builds a full book of pages from the given source text. + * @param source The source text to convert. + * @return A book containing the source text formatted for a minecraft book. + */ + public Book build(String source) { + final int maxLines = ApplicationProperties.getIntProp("book.page_max_lines"); + List lines = this.convertSourceToLines(source); + Book book = new Book(); + BookPage page = new BookPage(); + int currentPageLineCount = 0; + + for (String line : lines) { + page.addLine(line); + currentPageLineCount++; + if (currentPageLineCount == maxLines) { + book.addPage(page); + page = new BookPage(); + currentPageLineCount = 0; + } + } + + if (page.hasContent()) { + book.addPage(page); + } + + return book; + } + + /** + * Converts the given source string into a formatted list of lines that can + * be copied to a minecraft book. + * @param source The source string. + * @return A list of lines. + */ + private List convertSourceToLines(String source) { + List lines = new ArrayList<>(); + final char[] sourceChars = source.toCharArray(); + final int maxLinePixelWidth = ApplicationProperties.getIntProp("book.page_max_width"); + int sourceIndex = 0; + StringBuilder lineBuilder = new StringBuilder(64); + int linePixelWidth = 0; + StringBuilder symbolBuilder = new StringBuilder(64); + + while (sourceIndex < sourceChars.length) { + final char c = sourceChars[sourceIndex]; + sourceIndex++; + symbolBuilder.setLength(0); + symbolBuilder.append(c); + int symbolWidth = this.charWidthMapper.getWidth(c); + + // Since there's a 1-pixel gap between characters, add it to the width if this isn't the first char. + if (lineBuilder.length() > 0) { + symbolWidth++; + } + + // If we encounter a non-newline whitespace at the beginning of the line, skip it. + if (c == ' ' && lineBuilder.length() == 0) { + continue; + } + + // If we encounter a newline, immediately skip to a new line. + if (c == '\n') { + lines.add(lineBuilder.toString()); + lineBuilder.setLength(0); + linePixelWidth = 0; + continue; + } + + // If we encounter a word, keep accepting characters until we reach the end. + if (Character.isLetterOrDigit(c)) { + while ( + sourceIndex < sourceChars.length + && Character.isLetterOrDigit(sourceChars[sourceIndex]) + ) { + char nextChar = sourceChars[sourceIndex]; + symbolBuilder.append(nextChar); + symbolWidth += 1 + this.charWidthMapper.getWidth(nextChar); + sourceIndex++; + } + } + + final String symbol = symbolBuilder.toString(); + // Check if we need to go to the next line to fit the symbol. + if (linePixelWidth + symbolWidth > maxLinePixelWidth) { + lines.add(lineBuilder.toString()); + lineBuilder.setLength(0); + linePixelWidth = 0; + } + + // Finally, append the symbol. + lineBuilder.append(symbol); + linePixelWidth += symbolWidth; + } + + // Append any remaining text. + if (lineBuilder.length() > 0) { + lines.add(lineBuilder.toString()); + } + + return lines; + } +} diff --git a/src/main/java/nl/andrewlalis/blockbookbinder/view/BookPreviewPanel.java b/src/main/java/nl/andrewlalis/blockbookbinder/view/BookPreviewPanel.java index ca1c237..35f710a 100644 --- a/src/main/java/nl/andrewlalis/blockbookbinder/view/BookPreviewPanel.java +++ b/src/main/java/nl/andrewlalis/blockbookbinder/view/BookPreviewPanel.java @@ -17,9 +17,15 @@ public class BookPreviewPanel extends JPanel { @Getter private Book book; private int currentPage = 0; + private final JTextArea previewPageTextArea; private final JLabel titleLabel; + private final JButton previousPageButton; + private final JButton nextPageButton; + private final JButton firstPageButton; + private final JButton lastPageButton; + public BookPreviewPanel() { super(new BorderLayout()); @@ -44,22 +50,38 @@ public class BookPreviewPanel extends JPanel { this.add(previewPageScrollPane, BorderLayout.CENTER); JPanel previewButtonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); - JButton previousPageButton = new JButton("Previous Page"); - previousPageButton.addActionListener(e -> { + this.firstPageButton = new JButton("First"); + this.firstPageButton.addActionListener(e -> { + this.currentPage = 0; + displayCurrentPage(); + }); + + this.previousPageButton = new JButton("Previous Page"); + this.previousPageButton.addActionListener(e -> { if (currentPage > 0) { currentPage--; displayCurrentPage(); } }); - JButton nextPageButton = new JButton("Next Page"); - nextPageButton.addActionListener(e -> { + + this.nextPageButton = new JButton("Next Page"); + this.nextPageButton.addActionListener(e -> { if (currentPage < book.getPageCount() - 1) { currentPage++; displayCurrentPage(); } }); - previewButtonPanel.add(previousPageButton); - previewButtonPanel.add(nextPageButton); + + this.lastPageButton = new JButton("Last"); + this.lastPageButton.addActionListener(e -> { + this.currentPage = Math.max(this.book.getPageCount() - 1, 0); + displayCurrentPage(); + }); + + previewButtonPanel.add(this.firstPageButton); + previewButtonPanel.add(this.previousPageButton); + previewButtonPanel.add(this.nextPageButton); + previewButtonPanel.add(this.lastPageButton); this.add(previewButtonPanel, BorderLayout.SOUTH); this.setBook(new Book()); @@ -84,4 +106,11 @@ public class BookPreviewPanel extends JPanel { this.currentPage = page; this.displayCurrentPage(); } + + public void enableNavigation(boolean enabled) { + this.firstPageButton.setEnabled(enabled); + this.previousPageButton.setEnabled(enabled); + this.nextPageButton.setEnabled(enabled); + this.lastPageButton.setEnabled(enabled); + } } diff --git a/src/main/java/nl/andrewlalis/blockbookbinder/view/MainFrame.java b/src/main/java/nl/andrewlalis/blockbookbinder/view/MainFrame.java index cd591ee..4f83b40 100644 --- a/src/main/java/nl/andrewlalis/blockbookbinder/view/MainFrame.java +++ b/src/main/java/nl/andrewlalis/blockbookbinder/view/MainFrame.java @@ -65,8 +65,11 @@ public class MainFrame extends JFrame { JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); JButton exportButton = new JButton("Export to Book"); - exportButton.addActionListener(new BookExportActionListener(bookPreviewPanel)); + JButton cancelExportButton = new JButton("Cancel Export"); + cancelExportButton.setEnabled(false); + exportButton.addActionListener(new BookExportActionListener(bookPreviewPanel, cancelExportButton)); bottomPanel.add(exportButton); + bottomPanel.add(cancelExportButton); mainPanel.add(bottomPanel, BorderLayout.SOUTH); return mainPanel; diff --git a/src/main/java/nl/andrewlalis/blockbookbinder/view/SourceTextPanel.java b/src/main/java/nl/andrewlalis/blockbookbinder/view/SourceTextPanel.java index 95ff0a8..b1af537 100644 --- a/src/main/java/nl/andrewlalis/blockbookbinder/view/SourceTextPanel.java +++ b/src/main/java/nl/andrewlalis/blockbookbinder/view/SourceTextPanel.java @@ -5,6 +5,10 @@ import nl.andrewlalis.blockbookbinder.control.ConvertToBookActionListener; import javax.swing.*; import javax.swing.border.EmptyBorder; import java.awt.*; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; /** * A panel dedicated to displaying an interacting with a raw source of text for @@ -22,6 +26,11 @@ public class SourceTextPanel extends JPanel { this.textArea = new JTextArea(); this.textArea.setWrapStyleWord(true); this.textArea.setLineWrap(true); + try { + this.textArea.setText(Files.readString(Path.of(this.getClass().getClassLoader().getResource("sample/lorem_ipsum.txt").toURI()))); + } catch (IOException | URISyntaxException e) { + e.printStackTrace(); + } JScrollPane scrollWrappedMainTextArea = new JScrollPane(this.textArea, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); this.add(scrollWrappedMainTextArea, BorderLayout.CENTER); diff --git a/src/main/resources/sample/lorem_ipsum.txt b/src/main/resources/sample/lorem_ipsum.txt new file mode 100644 index 0000000..af0cb2b --- /dev/null +++ b/src/main/resources/sample/lorem_ipsum.txt @@ -0,0 +1,9 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut varius nulla non neque interdum tincidunt. Aenean nec lobortis mi. Phasellus id lacus lacinia, consectetur libero aliquam, facilisis enim. Proin nibh lorem, suscipit sit amet semper et, pulvinar et mi. Cras suscipit nibh purus, nec molestie arcu volutpat vitae. Etiam a sagittis mauris. Nunc luctus in lectus ut consectetur. Donec vitae gravida quam. Sed at commodo tortor. Mauris suscipit nec metus quis molestie. Suspendisse volutpat turpis at sapien tincidunt maximus. Aenean viverra eget metus in ultrices. + +Praesent risus metus, volutpat in pretium rhoncus, hendrerit sed nulla. Quisque nec libero sit amet metus fringilla bibendum. Mauris tempus, leo fringilla consectetur tincidunt, tellus dolor varius massa, sit amet dictum mi augue eu odio. Aliquam ut consequat libero. Vestibulum vitae interdum ligula. Vestibulum posuere orci non dapibus iaculis. Suspendisse sollicitudin volutpat nulla in tincidunt. Morbi nec egestas enim. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Quisque at ex pellentesque, rhoncus sapien ac, sodales sapien. Nunc id justo pretium tortor semper dignissim. Morbi vitae facilisis neque, a fringilla urna. Etiam aliquam efficitur fringilla. + +In blandit enim sem, id porta ligula fringilla nec. Mauris velit ex, finibus convallis risus et, hendrerit elementum justo. Suspendisse potenti. Etiam blandit tellus ac massa pharetra suscipit. Pellentesque viverra scelerisque porta. Integer ac dolor sed lorem tempus facilisis. Nunc egestas lorem ut nisl ultricies rhoncus. Nulla vitae risus ullamcorper, porttitor risus ac, vehicula neque. Pellentesque ac aliquam risus. Proin eu eros ac lacus viverra rutrum. Sed non nibh sapien. Phasellus sagittis rhoncus magna nec semper. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam eget nunc ante. Mauris congue lectus id nunc varius, vitae laoreet orci condimentum. Phasellus accumsan iaculis magna, porttitor maximus lacus pharetra a. + +Suspendisse potenti. Phasellus tempor nisi vitae dictum vestibulum. Curabitur vehicula tellus lacus, quis euismod sem eleifend ut. Donec blandit condimentum dui a sagittis. Cras dolor felis, consectetur id ipsum et, faucibus bibendum dui. Donec elementum tristique turpis at facilisis. Fusce vitae ipsum in orci gravida facilisis. Ut congue ullamcorper feugiat. Nullam condimentum diam a enim facilisis, quis aliquet arcu tristique. Nulla lacinia massa ac ultricies varius. Vestibulum vehicula sapien nibh, in pharetra velit mattis vitae. Aenean congue varius nulla, vel interdum metus faucibus nec. Ut augue erat, rhoncus vitae lacinia vel, elementum sit amet erat. + +Cras mollis quam pulvinar ultrices semper. Suspendisse et nisi ac ligula mattis tempus. Quisque vel dictum turpis. Donec in pellentesque turpis. Sed a fermentum libero, consectetur pulvinar quam. Ut efficitur, metus eu molestie semper, augue tortor vehicula diam, quis interdum erat tellus a odio. Etiam augue mauris, sollicitudin nec mollis sed, ornare in velit. Cras nec velit vitae mi tincidunt malesuada. Sed sagittis purus dapibus, molestie ante sit amet, accumsan libero. Quisque viverra sem ac purus varius vestibulum. Integer dictum cursus aliquet. Nullam non odio non justo faucibus volutpat non convallis lacus. Cras tempor ante erat, et ultrices nisi dictum ut. Aliquam scelerisque nunc turpis, imperdiet sollicitudin massa iaculis sit amet. Suspendisse mi lectus, pulvinar vel semper non, tristique vitae augue.