diff --git a/.gitignore b/.gitignore index 6dbb7902..ebb2c356 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ logfiles/ .idea/ **/.flattened-pom.xml /develop-logging.properties +/target/ diff --git a/app/src/main/scala/app/logorrr/conf/LogoRRRGlobals.scala b/app/src/main/scala/app/logorrr/conf/LogoRRRGlobals.scala index a3ad9af6..94a1972b 100644 --- a/app/src/main/scala/app/logorrr/conf/LogoRRRGlobals.scala +++ b/app/src/main/scala/app/logorrr/conf/LogoRRRGlobals.scala @@ -23,9 +23,7 @@ object LogoRRRGlobals extends CanLog { private val hostServicesProperty = new SimpleObjectProperty[LogoRRRHostServices]() - def persist(): Unit = { - persist(LogoRRRGlobals.getSettings) - } + def persist(): Unit = persist(LogoRRRGlobals.getSettings) def persist(settings: Settings): Unit = { Fs.write(FilePaths.settingsFilePath, ConfigWriter[Settings].to(settings).render(renderOptions)) diff --git a/app/src/main/scala/app/logorrr/conf/mut/MutLogFileSettings.scala b/app/src/main/scala/app/logorrr/conf/mut/MutLogFileSettings.scala index bbcfa83e..208a65af 100644 --- a/app/src/main/scala/app/logorrr/conf/mut/MutLogFileSettings.scala +++ b/app/src/main/scala/app/logorrr/conf/mut/MutLogFileSettings.scala @@ -2,13 +2,15 @@ package app.logorrr.conf.mut import app.logorrr.conf.BlockSettings import app.logorrr.io.FileId -import app.logorrr.model.{LogEntryInstantFormat, LogFileSettings} +import app.logorrr.model.{LogEntry, LogFileSettings, TimestampSettings} import app.logorrr.util.LogoRRRFonts -import app.logorrr.views.search.Filter +import app.logorrr.views.search.{AnyFilter, Filter, FilterButton, Fltr} import javafx.beans.binding.{BooleanBinding, StringBinding} import javafx.beans.property._ -import javafx.collections.FXCollections +import javafx.collections.transformation.FilteredList +import javafx.collections.{FXCollections, ObservableList} +import java.time.format.DateTimeFormatter import scala.jdk.CollectionConverters._ object MutLogFileSettings { @@ -22,8 +24,14 @@ object MutLogFileSettings { s.firstOpenedProperty.set(logFileSettings.firstOpened) s.setDividerPosition(logFileSettings.dividerPosition) s.setFilters(logFileSettings.filters) - s.someLogEntrySettingsProperty.set(logFileSettings.someLogEntryInstantFormat) + s.someTimestampSettings.set(logFileSettings.someTimestampSettings) + logFileSettings.someTimestampSettings match { + case Some(sts) => s.setDateTimeFormatter(sts.dateTimeFormatter) + case None => + } s.setAutoScroll(logFileSettings.autoScroll) + s.setLowerTimestamp(logFileSettings.lowerTimestamp) + s.setUpperTimestamp(logFileSettings.upperTimestamp) s } } @@ -31,28 +39,80 @@ object MutLogFileSettings { class MutLogFileSettings { + var someUnclassifiedFilter: Option[(Filter, FilterButton)] = None + var filterButtons: Map[Filter, FilterButton] = Map[Filter, FilterButton]() + + /** + * Filters are only active if selected. + * + * UnclassifiedFilter gets an extra handling since it depends on other filters + * + * @return + */ + def computeCurrentFilter(): Fltr = { + new AnyFilter(someUnclassifiedFilter.map(fst => if (fst._2.isSelected) Set(fst._1) else Set()).getOrElse(Set()) ++ + filterButtons.filter(fst => fst._2.isSelected).keySet) + } + + /** + * Reduce current displayed log entries by applying text filters and consider also the time stamp range. + * + * @param filteredList list to filter + */ + def updateActiveFilter(filteredList: FilteredList[LogEntry]): Unit = { + filteredList.setPredicate((entry: LogEntry) => + (entry.someInstant match { + case None => true // if instant is not set, return true + case Some(value) => + val asMilli = value.toEpochMilli + getLowTimestampBoundary <= asMilli && asMilli <= getHighTimestampBoundary + }) && computeCurrentFilter().matches(entry.value)) + } + + private val fileIdProperty = new SimpleObjectProperty[FileId]() private val firstOpenedProperty = new SimpleLongProperty() + private val someTimestampSettings = new SimpleObjectProperty[Option[TimestampSettings]](None) + private val dateTimeFormatterProperty = new SimpleObjectProperty[DateTimeFormatter](TimestampSettings.DefaultFormatter) + + val fontSizeProperty = new SimpleIntegerProperty() + val blockSizeProperty = new SimpleIntegerProperty() val selectedLineNumberProperty = new SimpleIntegerProperty() val firstVisibleTextCellIndexProperty = new SimpleIntegerProperty() val lastVisibleTextCellIndexProperty = new SimpleIntegerProperty() - val dividerPositionProperty = new SimpleDoubleProperty() - val fontSizeProperty = new SimpleIntegerProperty() + private val lowerTimestampProperty = new SimpleLongProperty(LogFileSettings.DefaultLowerTimestamp) + private val upperTimestampProperty = new SimpleLongProperty(LogFileSettings.DefaultUpperTimestamp) + + def setLowerTimestamp(lowerValue: Long): Unit = lowerTimestampProperty.set(lowerValue) + def getLowTimestampBoundary: Long = lowerTimestampProperty.get() + + def setUpperTimestamp(upperValue: Long): Unit = upperTimestampProperty.set(upperValue) + + def getHighTimestampBoundary: Long = upperTimestampProperty.get() + + val dividerPositionProperty = new SimpleDoubleProperty() val autoScrollActiveProperty = new SimpleBooleanProperty() val filtersProperty = new SimpleListProperty[Filter](FXCollections.observableArrayList()) - val someLogEntrySettingsProperty = new SimpleObjectProperty[Option[LogEntryInstantFormat]](None) - val blockSizeProperty = new SimpleIntegerProperty() + + def getSomeTimestampSettings: Option[TimestampSettings] = someTimestampSettings.get() + + def getDateTimeFormatter: DateTimeFormatter = dateTimeFormatterProperty.get() + + def setDateTimeFormatter(dateTimeFormatter: DateTimeFormatter): Unit = dateTimeFormatterProperty.set(dateTimeFormatter) def setFilters(filters: Seq[Filter]): Unit = { filtersProperty.setAll(filters.asJava) } + def getFilters: ObservableList[Filter] = filtersProperty.get() + + val hasLogEntrySettingBinding: BooleanBinding = new BooleanBinding { - bind(someLogEntrySettingsProperty) + bind(someTimestampSettings) override def computeValue(): Boolean = { - Option(someLogEntrySettingsProperty.get()).exists(_.isDefined) + Option(someTimestampSettings.get()).exists(_.isDefined) } } @@ -66,8 +126,12 @@ class MutLogFileSettings { override def computeValue(): String = LogoRRRFonts.jetBrainsMono(fontSizeProperty.get()) } - def setLogEntryInstantFormat(lef: LogEntryInstantFormat): Unit = { - someLogEntrySettingsProperty.set(Option(lef)) + def setSomeLogEntryInstantFormat(someLef: Option[TimestampSettings]): Unit = { + someTimestampSettings.set(someLef) + someLef match { + case Some(value) => setDateTimeFormatter(value.dateTimeFormatter) + case None => setDateTimeFormatter(null) + } } def setAutoScroll(autoScroll: Boolean): Unit = autoScrollActiveProperty.set(autoScroll) @@ -101,12 +165,14 @@ class MutLogFileSettings { , firstOpenedProperty.get() , dividerPositionProperty.get() , fontSizeProperty.get() - , filtersProperty.get().asScala.toSeq + , getFilters.asScala.toSeq , BlockSettings(blockSizeProperty.get()) - , someLogEntrySettingsProperty.get() + , someTimestampSettings.get() , autoScrollActiveProperty.get() , firstVisibleTextCellIndexProperty.get() - , lastVisibleTextCellIndexProperty.get()) + , lastVisibleTextCellIndexProperty.get() + , lowerTimestampProperty.get() + , upperTimestampProperty.get()) lfs } } diff --git a/app/src/main/scala/app/logorrr/io/IoManager.scala b/app/src/main/scala/app/logorrr/io/IoManager.scala index ca777526..363644c4 100644 --- a/app/src/main/scala/app/logorrr/io/IoManager.scala +++ b/app/src/main/scala/app/logorrr/io/IoManager.scala @@ -1,11 +1,12 @@ package app.logorrr.io -import app.logorrr.model.{LogEntry, LogEntryInstantFormat} +import app.logorrr.model.{LogEntry, TimestampSettings} import app.logorrr.util.{CanLog, OsUtil} import javafx.collections.{FXCollections, ObservableList} import java.io._ import java.nio.file.{Files, Path} +import java.time.{Duration, Instant} import java.util import java.util.zip.{ZipEntry, ZipInputStream} import scala.util.{Failure, Success, Try} @@ -58,7 +59,7 @@ object IoManager extends CanLog { val arraylist = new java.util.ArrayList[LogEntry]() toSeq(mkReader(asBytes)).map(l => { lineNumber = lineNumber + 1 - arraylist.add(LogEntry(lineNumber, l, None)) + arraylist.add(LogEntry(lineNumber, l, None, None)) }) FXCollections.observableList(arraylist) @@ -69,22 +70,34 @@ object IoManager extends CanLog { val arraylist = new java.util.ArrayList[LogEntry]() fromPathUsingSecurityBookmarks(logFile).map(l => { lineNumber = lineNumber + 1 - arraylist.add(LogEntry(lineNumber, l, None)) + arraylist.add(LogEntry(lineNumber, l, None, None)) }) FXCollections.observableList(arraylist) } - def from(logFile: Path, logEntryTimeFormat: LogEntryInstantFormat): ObservableList[LogEntry] = { + def from(logFile: Path, logEntryTimeFormat: TimestampSettings): ObservableList[LogEntry] = { var lineNumber: Int = 0 + var someFirstEntryTimestamp: Option[Instant] = None val arraylist = new util.ArrayList[LogEntry]() fromPathUsingSecurityBookmarks(logFile).map(l => { lineNumber = lineNumber + 1 - arraylist.add(LogEntry(lineNumber, l, LogEntryInstantFormat.parseInstant(l, logEntryTimeFormat))) + val someInstant: Option[Instant] = TimestampSettings.parseInstant(l, logEntryTimeFormat) + if (someFirstEntryTimestamp.isEmpty) { + someFirstEntryTimestamp = someInstant + } + + val diffFromStart: Option[Duration] = for { + firstEntry <- someFirstEntryTimestamp + instant <- someInstant + } yield Duration.between(firstEntry, instant) + + // first entry + arraylist.add(LogEntry(lineNumber, l, someInstant, diffFromStart)) }) FXCollections.observableList(arraylist) } - def readEntries(path : Path, someLogEntryInstantFormat: Option[LogEntryInstantFormat]): ObservableList[LogEntry] = { + def readEntries(path: Path, someLogEntryInstantFormat: Option[TimestampSettings]): ObservableList[LogEntry] = { if (isPathValid(path)) { Try(someLogEntryInstantFormat match { case None => IoManager.from(path) @@ -102,7 +115,7 @@ object IoManager extends CanLog { } } - def isPathValid(path : Path): Boolean = + def isPathValid(path: Path): Boolean = if (OsUtil.isMac) { Files.exists(path) } else { @@ -143,7 +156,6 @@ object IoManager extends CanLog { } - - def isZip(path : Path) : Boolean = path.getFileName.toString.endsWith(".zip") + def isZip(path: Path): Boolean = path.getFileName.toString.endsWith(".zip") } diff --git a/app/src/main/scala/app/logorrr/model/LogEntry.scala b/app/src/main/scala/app/logorrr/model/LogEntry.scala index ab7d6468..a08a8fc8 100644 --- a/app/src/main/scala/app/logorrr/model/LogEntry.scala +++ b/app/src/main/scala/app/logorrr/model/LogEntry.scala @@ -1,15 +1,21 @@ package app.logorrr.model -import java.time.Instant +import java.time.{Duration, Instant} /** * represents one line in a log file * - * @param lineNumber line number of this log entry - * @param value contens of line in plaintext + * @param lineNumber line number of this log entry + * @param value contens of line in plaintext * @param someInstant a timestamp if there is any * */ case class LogEntry(lineNumber: Int , value: String - , someInstant: Option[Instant]) + , someInstant: Option[Instant] + , someDurationSinceFirstInstant: Option[Duration]) { + + /** returns a copy of this log entry without timestamp information */ + def withOutTimestamp(): LogEntry = copy(someInstant = None, someDurationSinceFirstInstant = None) + +} diff --git a/app/src/main/scala/app/logorrr/model/LogEntryInstantFormat.scala b/app/src/main/scala/app/logorrr/model/LogEntryInstantFormat.scala deleted file mode 100644 index 99f143c0..00000000 --- a/app/src/main/scala/app/logorrr/model/LogEntryInstantFormat.scala +++ /dev/null @@ -1,46 +0,0 @@ -package app.logorrr.model - -import app.logorrr.util.CanLog -import app.logorrr.views.settings.timer.SimpleRange -import pureconfig.{ConfigReader, ConfigWriter} -import pureconfig.generic.semiauto.{deriveReader, deriveWriter} - -import java.time.format.DateTimeFormatter -import java.time.{Instant, LocalDateTime, ZoneId, ZoneOffset} -import scala.util.{Failure, Success, Try} - -object LogEntryInstantFormat extends CanLog { - - val DefaultPattern = "yyyy-MM-dd HH:mm:ss.nnnnnnnnn" - /** just my preferred time format */ - val Default: LogEntryInstantFormat = LogEntryInstantFormat(SimpleRange(1, 24), DefaultPattern) - - implicit lazy val reader: ConfigReader[LogEntryInstantFormat] = deriveReader[LogEntryInstantFormat] - implicit lazy val writer: ConfigWriter[LogEntryInstantFormat] = deriveWriter[LogEntryInstantFormat] - - def parseInstant(line: String, entrySetting: LogEntryInstantFormat): Option[Instant] = - if (line.length >= entrySetting.endCol) { - val dateTimeAsString = line.substring(entrySetting.startCol, entrySetting.endCol) - Try { - val dtf: DateTimeFormatter = entrySetting.dateTimeFormatter - LocalDateTime.parse(dateTimeAsString, dtf).toInstant(ZoneOffset.of(entrySetting.zoneOffset)) - } match { - case Success(value) => Option(value) - case Failure(_) => - logTrace(s"Could not parse '$dateTimeAsString' at pos (${entrySetting.startCol}/${entrySetting.endCol}) with pattern '$${entrySetting.dateTimePattern}'") - None - } - } else { - None - } - - -} - -case class LogEntryInstantFormat(range: SimpleRange - , dateTimePattern: String - , zoneOffset: String = "+1") { - val startCol: Int = range.start - val endCol: Int = range.end - val dateTimeFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern(dateTimePattern).withZone(ZoneId.of(zoneOffset)) -} \ No newline at end of file diff --git a/app/src/main/scala/app/logorrr/model/LogFileSettings.scala b/app/src/main/scala/app/logorrr/model/LogFileSettings.scala index 31e42e7e..7d79f74b 100644 --- a/app/src/main/scala/app/logorrr/model/LogFileSettings.scala +++ b/app/src/main/scala/app/logorrr/model/LogFileSettings.scala @@ -18,10 +18,12 @@ object LogFileSettings { private val DefaultSelectedIndex = 0 private val DefaultDividerPosition = 0.5 private val DefaultBlockSettings = BlockSettings(10) - private val DefaultLogFormat: Option[LogEntryInstantFormat] = None + private val DefaultLogFormat: Option[TimestampSettings] = None private val DefaultAutoScroll = false private val DefaultFirstViewIndex = -1 private val DefaultLastViewIndex = -1 + val DefaultLowerTimestamp: Int = 0 + val DefaultUpperTimestamp: Long = Instant.now().toEpochMilli private val FinestFilter: Filter = new Filter("FINEST", Color.GREY, true) private val InfoFilter: Filter = new Filter("INFO", Color.GREEN, true) private val WarningFilter: Filter = new Filter("WARNING", Color.ORANGE, true) @@ -41,7 +43,9 @@ object LogFileSettings { , DefaultLogFormat , DefaultAutoScroll , DefaultFirstViewIndex - , DefaultLastViewIndex) + , DefaultLastViewIndex + , DefaultLowerTimestamp + , Instant.now().toEpochMilli) } } @@ -62,7 +66,7 @@ object LogFileSettings { * @param fontSize font size to use * @param filters filters which should be applied * @param blockSettings settings for the left view - * @param someLogEntryInstantFormat used timestamp format + * @param someTimestampSettings used timestamp format * @param autoScroll true if 'follow mode' is active * @param firstVisibleTextCellIndex which index is the first visible on the screen (depending on resolution, window size ...) * @param lastVisibleTextCellIndex which index is the last visible on the screen (depending on resolution, window size ...) @@ -74,10 +78,12 @@ case class LogFileSettings(fileId: FileId , fontSize: Int , filters: Seq[Filter] , blockSettings: BlockSettings - , someLogEntryInstantFormat: Option[LogEntryInstantFormat] + , someTimestampSettings: Option[TimestampSettings] , autoScroll: Boolean , firstVisibleTextCellIndex: Int - , lastVisibleTextCellIndex: Int) { + , lastVisibleTextCellIndex: Int + , lowerTimestamp: Long + , upperTimestamp: Long) { val path: Path = fileId.asPath.toAbsolutePath diff --git a/app/src/main/scala/app/logorrr/model/TimestampSettings.scala b/app/src/main/scala/app/logorrr/model/TimestampSettings.scala new file mode 100644 index 00000000..5f5925b4 --- /dev/null +++ b/app/src/main/scala/app/logorrr/model/TimestampSettings.scala @@ -0,0 +1,65 @@ +package app.logorrr.model + +import app.logorrr.util.CanLog +import app.logorrr.views.settings.timestamp.SimpleRange +import pureconfig.generic.semiauto.{deriveReader, deriveWriter} +import pureconfig.{ConfigReader, ConfigWriter} + +import java.time._ +import java.time.format.DateTimeFormatter +import scala.util.{Failure, Success, Try} + +object TimestampSettings extends CanLog { + + val DefaultPattern = "yyyy-MM-dd HH:mm:ss.SSS" + + val DefaultFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern(DefaultPattern) + + val Default: TimestampSettings = TimestampSettings(SimpleRange(1, 24), DefaultPattern) + + implicit lazy val reader: ConfigReader[TimestampSettings] = deriveReader[TimestampSettings] + implicit lazy val writer: ConfigWriter[TimestampSettings] = deriveWriter[TimestampSettings] + + /** + * Assumes that line contains a timestamp which encodes year/month/day hour/minute/second ... which hits + * most of the usecases. However, for example if the log file uses relative timestamps this approach won't fit. + * + * @param line the string to parse + * @param leif settings where to parse the timestamp, and in which format + * @return + */ + def parseInstant(line: String, leif: TimestampSettings): Option[Instant] = + if (line.length >= leif.endCol) { + val dateTimeAsString = line.substring(leif.startCol, leif.endCol) + val dtf: DateTimeFormatter = leif.dateTimeFormatter + Try { + LocalDateTime.parse(dateTimeAsString, dtf).atZone(ZoneId.systemDefault).toInstant + } match { + case Success(value) => Option(value) + case Failure(_) => + // retrying with localtime as fallback for entries which don't have any + // date information (for example: '08:34:33' representing today morning) + Try { + LocalDateTime.of(LocalDate.now(), LocalTime.parse(dateTimeAsString, dtf)).atZone(ZoneId.systemDefault()).toInstant + } match { + case Success(value) => Option(value) + case Failure(exception) => + logTrace(s"Could not be parsed: '$dateTimeAsString' at pos (${leif.startCol}/${leif.endCol}) using pattern '${leif.dateTimePattern}': ${exception.getMessage}") + None + } + } + } else { + None + } + + +} + +case class TimestampSettings(range: SimpleRange, dateTimePattern: String) { + val startCol: Int = range.start + val endCol: Int = range.end + val dateTimeFormatter: DateTimeFormatter = { + // if we can't parse the provided pattern, fallback to default - even if we can't parse timestamps then + Try(DateTimeFormatter.ofPattern(dateTimePattern).withZone(ZoneId.systemDefault)).toOption.getOrElse(TimestampSettings.DefaultFormatter) + } +} \ No newline at end of file diff --git a/app/src/main/scala/app/logorrr/util/JfxUtils.scala b/app/src/main/scala/app/logorrr/util/JfxUtils.scala index 084d43b0..0a378534 100644 --- a/app/src/main/scala/app/logorrr/util/JfxUtils.scala +++ b/app/src/main/scala/app/logorrr/util/JfxUtils.scala @@ -4,11 +4,25 @@ import javafx.application.Platform import javafx.beans.{InvalidationListener, Observable} import javafx.beans.value.{ChangeListener, ObservableValue} import javafx.collections.ListChangeListener -import javafx.scene.control.ListView +import javafx.geometry.Pos +import javafx.scene.control.{Label, ListView, TextField} import javafx.stage.{Stage, WindowEvent} object JfxUtils extends CanLog { + + def mkTextField(width: Double): TextField = { + val t = new TextField() + t.setPrefWidth(width) + t + } + + def mkL(text: String, width: Double): Label = { + val l = new Label(text) + l.setPrefWidth(width) + l.setAlignment(Pos.CENTER_RIGHT) + l + } def scrollTo[T](lv: ListView[T], cellHeight: Int, relativeIndex: Int): Unit = { val visibleItemCount = (lv.getHeight / cellHeight).asInstanceOf[Int] / 2 lv.scrollTo(relativeIndex - visibleItemCount) diff --git a/app/src/main/scala/app/logorrr/views/autoscroll/LogEntryListener.scala b/app/src/main/scala/app/logorrr/views/autoscroll/LogEntryListener.scala index 99704c70..02d66b29 100644 --- a/app/src/main/scala/app/logorrr/views/autoscroll/LogEntryListener.scala +++ b/app/src/main/scala/app/logorrr/views/autoscroll/LogEntryListener.scala @@ -21,7 +21,7 @@ class LogEntryListener(ol: ObservableList[LogEntry]) override def handle(l: String): Unit = { currentCnt = currentCnt + 1 - val e = LogEntry(currentCnt, l, None) + val e = LogEntry(currentCnt, l, None, None) JfxUtils.execOnUiThread(ol.add(e)) } diff --git a/app/src/main/scala/app/logorrr/views/block/ChunkListView.scala b/app/src/main/scala/app/logorrr/views/block/ChunkListView.scala index 2b801a34..ade70429 100644 --- a/app/src/main/scala/app/logorrr/views/block/ChunkListView.scala +++ b/app/src/main/scala/app/logorrr/views/block/ChunkListView.scala @@ -36,16 +36,16 @@ object ChunkListView { } def apply(entries: ObservableList[LogEntry] - , settings: MutLogFileSettings + , mutLogFileSettings: MutLogFileSettings , selectInTextView: LogEntry => Unit ): ChunkListView = { new ChunkListView(entries - , settings.selectedLineNumberProperty - , settings.blockSizeProperty - , settings.filtersProperty - , settings.dividerPositionProperty - , settings.firstVisibleTextCellIndexProperty - , settings.lastVisibleTextCellIndexProperty + , mutLogFileSettings.selectedLineNumberProperty + , mutLogFileSettings.blockSizeProperty + , mutLogFileSettings.filtersProperty + , mutLogFileSettings.dividerPositionProperty + , mutLogFileSettings.firstVisibleTextCellIndexProperty + , mutLogFileSettings.lastVisibleTextCellIndexProperty , selectInTextView) } } @@ -174,8 +174,13 @@ class ChunkListView(val logEntries: ObservableList[LogEntry] } } + /** invalidation listener has to be disabled when manipulating log entries (needed for setting the timestamp for example) */ + def addInvalidationListener(): Unit = logEntries.addListener(logEntriesInvalidationListener) + + def removeInvalidationListener(): Unit = logEntries.removeListener(logEntriesInvalidationListener) + private def addListeners(): Unit = { - logEntries.addListener(logEntriesInvalidationListener) + addInvalidationListener() selectedLineNumberProperty.addListener(selectedRp) firstVisibleTextCellIndexProperty.addListener(firstVisibleRp) lastVisibleTextCellIndexProperty.addListener(lastVisibleRp) @@ -183,14 +188,11 @@ class ChunkListView(val logEntries: ObservableList[LogEntry] widthProperty().addListener(widthRp) heightProperty().addListener(heightRp) skinProperty().addListener(chunkListViewSkinListener) - - } def removeListeners(): Unit = { - logEntries.removeListener(logEntriesInvalidationListener) - + removeInvalidationListener() selectedLineNumberProperty.removeListener(selectedRp) firstVisibleTextCellIndexProperty.removeListener(firstVisibleRp) lastVisibleTextCellIndexProperty.removeListener(lastVisibleRp) diff --git a/app/src/main/scala/app/logorrr/views/block/LPixelBuffer.scala b/app/src/main/scala/app/logorrr/views/block/LPixelBuffer.scala index 356e3af1..629dc140 100644 --- a/app/src/main/scala/app/logorrr/views/block/LPixelBuffer.scala +++ b/app/src/main/scala/app/logorrr/views/block/LPixelBuffer.scala @@ -213,11 +213,13 @@ case class LPixelBuffer(blockNumber: Int updateBuffer((_: PixelBuffer[IntBuffer]) => { cleanBackground() var i = 0 - entries.forEach(e => { - val color = Filter.calcColor(e.value, filters) - paintBlock(i, e.lineNumber, color) - i = i + 1 - }) + if (!entries.isEmpty) { + entries.forEach(e => { + val color = Filter.calcColor(e.value, filters) + paintBlock(i, e.lineNumber, color) + i = i + 1 + }) + } shape }) } diff --git a/app/src/main/scala/app/logorrr/views/logfiletab/LogFileTab.scala b/app/src/main/scala/app/logorrr/views/logfiletab/LogFileTab.scala index 37caea8b..ef7f6aed 100644 --- a/app/src/main/scala/app/logorrr/views/logfiletab/LogFileTab.scala +++ b/app/src/main/scala/app/logorrr/views/logfiletab/LogFileTab.scala @@ -7,6 +7,7 @@ import app.logorrr.model.LogEntry import app.logorrr.util._ import app.logorrr.views.autoscroll.LogTailer import app.logorrr.views.logfiletab.actions._ +import app.logorrr.views.main.MainTabPane import app.logorrr.views.search.Fltr import app.logorrr.views.{LogoRRRAccelerators, UiNode, UiNodeFileIdAware} import javafx.beans.binding.Bindings @@ -81,7 +82,7 @@ class LogFileTab(val fileId: FileId private lazy val logTailer = LogTailer(fileId, entries) - val logFileTabContent = new LogFileTabContent(fileId, mutLogFileSettings, entries) + val logFileTabContent = new LogFileTabContent(mutLogFileSettings, entries) private def startTailer(): Unit = { logFileTabContent.addTailerListener() @@ -197,8 +198,9 @@ class LogFileTab(val fileId: FileId Seq(new CloseLeftFilesMenuItem(fileId, this), new CloseRightFilesMenuItem(fileId, this)) } + val mergeTimedMenuItem = new MergeTimedMenuItem(fileId, this.getTabPane.asInstanceOf[MainTabPane]) - val items = { + val items: Seq[MenuItem] = { // special handling if there is only one tab if (getTabPane.getTabs.size() == 1) { if (OsUtil.isMac) { @@ -210,11 +212,11 @@ class LogFileTab(val fileId: FileId Seq(closeMenuItem , closeOtherFilesMenuItem , closeAllFilesMenuItem) ++ leftRightCloser ++ { - if (OsUtil.isMac) { + (if (OsUtil.isMac) { Seq() } else { Seq(openInFinderMenuItem) - } + }) ++ Seq(mergeTimedMenuItem) } } } diff --git a/app/src/main/scala/app/logorrr/views/logfiletab/LogFileTabContent.scala b/app/src/main/scala/app/logorrr/views/logfiletab/LogFileTabContent.scala index 4aa6912c..d73876e7 100644 --- a/app/src/main/scala/app/logorrr/views/logfiletab/LogFileTabContent.scala +++ b/app/src/main/scala/app/logorrr/views/logfiletab/LogFileTabContent.scala @@ -1,10 +1,10 @@ package app.logorrr.views.logfiletab import app.logorrr.conf.mut.MutLogFileSettings -import app.logorrr.io.FileId import app.logorrr.model.LogEntry import app.logorrr.views.block.ChunkListView import app.logorrr.views.ops.OpsRegion +import app.logorrr.views.ops.time.TimeOpsToolBar import app.logorrr.views.search.{Filter, FiltersToolBar, OpsToolBar} import app.logorrr.views.text.LogTextView import javafx.beans.{InvalidationListener, Observable} @@ -14,8 +14,7 @@ import javafx.scene.control.SplitPane import javafx.scene.layout.{BorderPane, VBox} -class LogFileTabContent(fileId: FileId - , mutLogFileSettings: MutLogFileSettings +class LogFileTabContent(mutLogFileSettings: MutLogFileSettings , val entries: ObservableList[LogEntry]) extends BorderPane { // make sure we have a white background for our tabs - see https://github.com/rladstaetter/LogoRRR/issues/188 @@ -30,14 +29,12 @@ class LogFileTabContent(fileId: FileId // graphical display to the left private val chunkListView = ChunkListView(filteredList, mutLogFileSettings, logTextView.scrollToItem) - private val blockSizeSlider = { val bs = new BlockSizeSlider(mutLogFileSettings.getFileId) bs.valueProperty().bindBidirectional(mutLogFileSettings.blockSizeProperty) bs } - private val blockPane = { val bBp = new BorderPane(chunkListView, blockSizeSlider, null, null, null) // vBox.setStyle("-fx-background-color: #b6ff7a;") @@ -58,15 +55,22 @@ class LogFileTabContent(fileId: FileId def removeTailerListener(): Unit = filteredList.removeListener(scrollToEndEventListener) - val opsToolBar = new OpsToolBar(mutLogFileSettings.getFileId, addFilter, entries, filteredList, mutLogFileSettings.blockSizeProperty) + val opsToolBar = new OpsToolBar(mutLogFileSettings.getFileId + , addFilter + , entries + , filteredList + , mutLogFileSettings.blockSizeProperty) private val filtersToolBar = { - val fbtb = new FiltersToolBar(fileId, filteredList, removeFilter) + val fbtb = new FiltersToolBar(mutLogFileSettings, filteredList, removeFilter) fbtb.filtersProperty.bind(mutLogFileSettings.filtersProperty) fbtb } - - private val opsRegion: OpsRegion = new OpsRegion(opsToolBar, filtersToolBar) + val timeOpsToolBar = new TimeOpsToolBar(mutLogFileSettings + , chunkListView + , entries // we write on this list potentially + , filteredList) + private val opsRegion: OpsRegion = new OpsRegion(opsToolBar, filtersToolBar, timeOpsToolBar) private val pane = new SplitPane(blockPane, logTextView) diff --git a/app/src/main/scala/app/logorrr/views/logfiletab/actions/CloseLeftFilesMenuItem.scala b/app/src/main/scala/app/logorrr/views/logfiletab/actions/CloseLeftFilesMenuItem.scala index 6a431d5b..e9eff77f 100644 --- a/app/src/main/scala/app/logorrr/views/logfiletab/actions/CloseLeftFilesMenuItem.scala +++ b/app/src/main/scala/app/logorrr/views/logfiletab/actions/CloseLeftFilesMenuItem.scala @@ -40,3 +40,10 @@ class CloseLeftFilesMenuItem(fileId: FileId, fileTab: => LogFileTab) extends Men }) } + + + + + + + diff --git a/app/src/main/scala/app/logorrr/views/logfiletab/actions/MergeTimedMenuItem.scala b/app/src/main/scala/app/logorrr/views/logfiletab/actions/MergeTimedMenuItem.scala new file mode 100644 index 00000000..8f600da7 --- /dev/null +++ b/app/src/main/scala/app/logorrr/views/logfiletab/actions/MergeTimedMenuItem.scala @@ -0,0 +1,84 @@ +package app.logorrr.views.logfiletab.actions + +import app.logorrr.io.FileId +import app.logorrr.model.LogEntry +import app.logorrr.views.logfiletab.LogFileTab +import app.logorrr.views.main.MainTabPane +import app.logorrr.views.{UiNode, UiNodeFileIdAware} +import javafx.scene.control.{Menu, MenuItem} + +import java.nio.file.Files +import java.util +import scala.jdk.CollectionConverters.CollectionHasAsScala + +object MergeTimedMenuItem extends UiNodeFileIdAware { + + override def uiNode(id: FileId): UiNode = UiNode(id, classOf[MergeTimedMenuItem]) + +} + +class CombineFileMenuItem(thisFileId: FileId + , otherFileId: FileId + , mainTabPane: MainTabPane) extends MenuItem(otherFileId.value) { + + setOnAction(_ => { + for {thisTab <- mainTabPane.getByFileId(thisFileId) + otherTab <- mainTabPane.getByFileId(otherFileId)} yield { + val list = new util.ArrayList[LogEntry]() + list.addAll(thisTab.entries) + list.addAll(otherTab.entries) + list.sort((o1: LogEntry, o2: LogEntry) => { + (o1.someInstant, o2.someInstant) match { + case (None, None) => 0 + case (None, Some(_)) => -1 + case (Some(_), None) => 1 + case (Some(d1), Some(d2)) => d1.compareTo(d2) + } + }) + val lines = new util.ArrayList[String]() + list.forEach((t: LogEntry) => lines.add(t.value)) + val fileName = thisFileId.fileName + "_" + otherFileId.fileName + val mergedPath = thisFileId.asPath.getParent.resolve(fileName) + Files.write(mergedPath, lines) + mainTabPane.addFile(FileId(mergedPath)) + } + }) + +} + + +class MergeTimedMenuItem(thisFileId: FileId, tabPane: MainTabPane) extends Menu("Merge file with ...") { + setId(MergeTimedMenuItem.uiNode(thisFileId).value) + + for (t <- tabPane.getTabs.asScala) { + val otherFileTab = t.asInstanceOf[LogFileTab] + if (otherFileTab.fileId != thisFileId && otherFileTab.mutLogFileSettings.hasLogEntrySettingBinding.get) { + getItems.add(new CombineFileMenuItem(thisFileId, otherFileTab.fileId, tabPane)) + } + } + + /* + setOnAction(_ => { + var deletethem = true + val toBeDeleted: Seq[Tab] = { + tabPane.getTabs.asScala.flatMap { t => + if (t.asInstanceOf[LogFileTab].fileId == fileTab.fileId) { + deletethem = false + None + } else { + if (deletethem) { + t.asInstanceOf[LogFileTab].shutdown() + Option(t) + } else { + None + } + } + }.toSeq + } + tabPane.getTabs.removeAll(toBeDeleted: _*) + // reinit context menu since there are no files left on the left side and thus the option should not be shown anymore + tabPane.getTabs.get(0).asInstanceOf[LogFileTab].initContextMenu() + + }) + */ +} \ No newline at end of file diff --git a/app/src/main/scala/app/logorrr/views/main/LogoRRRMain.scala b/app/src/main/scala/app/logorrr/views/main/LogoRRRMain.scala index ac26df65..b01b340a 100644 --- a/app/src/main/scala/app/logorrr/views/main/LogoRRRMain.scala +++ b/app/src/main/scala/app/logorrr/views/main/LogoRRRMain.scala @@ -82,7 +82,7 @@ class LogoRRRMain(closeStage: => Unit val fileBasedSettings: Seq[Future[Option[LogFileTab]]] = fileSettings.map(lfs => Future { timeR({ - val entries = IoManager.readEntries(lfs.path, lfs.someLogEntryInstantFormat) + val entries = IoManager.readEntries(lfs.path, lfs.someTimestampSettings) Option(LogFileTab(LogoRRRGlobals.getLogFileSettings(lfs.fileId), entries)) }, s"Loaded '${lfs.fileId.absolutePathAsString}' from filesystem ...") }) diff --git a/app/src/main/scala/app/logorrr/views/main/MainTabPane.scala b/app/src/main/scala/app/logorrr/views/main/MainTabPane.scala index bfaca6bd..c04a98f2 100644 --- a/app/src/main/scala/app/logorrr/views/main/MainTabPane.scala +++ b/app/src/main/scala/app/logorrr/views/main/MainTabPane.scala @@ -107,6 +107,8 @@ class MainTabPane extends TabPane with CanLog { getLogFileTabs.exists(lr => lr.fileId == p) } + def getByFileId(fileId: FileId): Option[LogFileTab] = getLogFileTabs.find(_.fileId == fileId) + def getLogFileTabs: mutable.Seq[LogFileTab] = getTabs.asScala.flatMap { _ match { case l: LogFileTab => Option(l) @@ -152,7 +154,7 @@ class MainTabPane extends TabPane with CanLog { def addFile(fileId: FileId): Unit = { val logFileSettings = LogFileSettings(fileId) LogoRRRGlobals.registerSettings(logFileSettings) - val entries = IoManager.readEntries(logFileSettings.path, logFileSettings.someLogEntryInstantFormat) + val entries = IoManager.readEntries(logFileSettings.path, logFileSettings.someTimestampSettings) addLogFileTab(LogFileTab(LogoRRRGlobals.getLogFileSettings(fileId), entries)) selectFile(fileId) } diff --git a/app/src/main/scala/app/logorrr/views/ops/OpsRegion.scala b/app/src/main/scala/app/logorrr/views/ops/OpsRegion.scala index 0a3b09b3..ef0f395b 100644 --- a/app/src/main/scala/app/logorrr/views/ops/OpsRegion.scala +++ b/app/src/main/scala/app/logorrr/views/ops/OpsRegion.scala @@ -1,20 +1,20 @@ package app.logorrr.views.ops +import app.logorrr.views.ops.time.TimeOpsToolBar import app.logorrr.views.search.{FiltersToolBar, OpsToolBar} -import javafx.geometry.Pos -import javafx.scene.layout.HBox +import javafx.scene.layout.VBox /** * Container to horizontally align search, filters and settings */ -class OpsRegion(opsToolBar: OpsToolBar, filtersToolBar: FiltersToolBar) extends HBox { - - HBox.setHgrow(filtersToolBar, javafx.scene.layout.Priority.ALWAYS) - setAlignment(Pos.CENTER_LEFT) - opsToolBar.setMaxHeight(Double.PositiveInfinity) - filtersToolBar.setMaxHeight(Double.PositiveInfinity) - getChildren.addAll(opsToolBar, filtersToolBar) +class OpsRegion(opsToolBar: OpsToolBar + , filtersToolBar: FiltersToolBar + , timeOpsToolBar: TimeOpsToolBar) extends VBox { + getChildren.addAll(timeOpsToolBar, new StdOpsToolBar(opsToolBar, filtersToolBar)) } + + + diff --git a/app/src/main/scala/app/logorrr/views/ops/StdOpsToolBar.scala b/app/src/main/scala/app/logorrr/views/ops/StdOpsToolBar.scala new file mode 100644 index 00000000..d9ca2320 --- /dev/null +++ b/app/src/main/scala/app/logorrr/views/ops/StdOpsToolBar.scala @@ -0,0 +1,15 @@ +package app.logorrr.views.ops + +import app.logorrr.views.search.{FiltersToolBar, OpsToolBar} +import javafx.geometry.Pos +import javafx.scene.layout.HBox + +class StdOpsToolBar(opsToolBar: OpsToolBar, filtersToolBar: FiltersToolBar) extends HBox { + + HBox.setHgrow(filtersToolBar, javafx.scene.layout.Priority.ALWAYS) + setAlignment(Pos.CENTER_LEFT) + opsToolBar.setMaxHeight(Double.PositiveInfinity) + filtersToolBar.setMaxHeight(Double.PositiveInfinity) + getChildren.addAll(opsToolBar, filtersToolBar) + +} diff --git a/app/src/main/scala/app/logorrr/views/ops/time/TimeOpsToolBar.scala b/app/src/main/scala/app/logorrr/views/ops/time/TimeOpsToolBar.scala new file mode 100644 index 00000000..25f93663 --- /dev/null +++ b/app/src/main/scala/app/logorrr/views/ops/time/TimeOpsToolBar.scala @@ -0,0 +1,72 @@ +package app.logorrr.views.ops.time + +import app.logorrr.conf.mut.MutLogFileSettings +import app.logorrr.model.LogEntry +import app.logorrr.views.block.ChunkListView +import javafx.collections.ObservableList +import javafx.collections.transformation.FilteredList +import javafx.scene.control.ToolBar + + +class TimeOpsToolBar(mutLogFileSettings: MutLogFileSettings + , chunkListView: ChunkListView + , logEntries: ObservableList[LogEntry] + , filteredList: FilteredList[LogEntry]) extends ToolBar { + + //val fileId: FileId = mutLogFileSettings.getFileId + // val formatter: DateTimeFormatter = Option(mutLogFileSettings.getDateTimeFormatter()).getOrElse(TimestampSettings.Default.dateTimeFormatter) + + private val lowSlider = new TimerSlider(mutLogFileSettings.hasLogEntrySettingBinding, "Configure earliest timestamp to be displayed") + private val highSlider = new TimerSlider(mutLogFileSettings.hasLogEntrySettingBinding, "Configure latest timestamp to be displayed") + private val leftLabel = new TimestampSliderLabel(mutLogFileSettings, lowSlider) + private val rightLabel = new TimestampSliderLabel(mutLogFileSettings, highSlider) + + /** + * To configure the logformat of the timestamp used in a logfile + */ + private val timestampSettingsButton = new TimestampSettingsButton(mutLogFileSettings, chunkListView, logEntries, this) + + lowSlider.setValue(lowSlider.getMin) + highSlider.setValue(highSlider.getMax) + + lowSlider.valueProperty.addListener((_, _, newValue) => { + if (newValue.doubleValue > highSlider.getValue) lowSlider.setValue(highSlider.getValue) + val lowValue = newValue.longValue() + val highValue = highSlider.getValue.longValue + mutLogFileSettings.setLowerTimestamp(lowValue) + mutLogFileSettings.setUpperTimestamp(highValue) + mutLogFileSettings.updateActiveFilter(filteredList) + // TimeOpsToolBar.updateFilteredList(mutLogFileSettings, filteredList, lowValue, highValue) + }) + + highSlider.valueProperty.addListener((_, _, newValue) => { + if (newValue.doubleValue < lowSlider.getValue) highSlider.setValue(lowSlider.getValue) + val lowValue = lowSlider.getValue.longValue + val highValue = newValue.longValue() + mutLogFileSettings.setLowerTimestamp(lowValue) + mutLogFileSettings.setUpperTimestamp(highValue) + mutLogFileSettings.updateActiveFilter(filteredList) + }) + + getItems.addAll(Seq(timestampSettingsButton, leftLabel, lowSlider, highSlider, rightLabel): _*) + + updateSliderBoundaries() + + def updateSliderBoundaries(): Unit = { + TimerSlider.calculateBoundaries(filteredList) match { + case Some((minInstant, maxInstant)) => + lowSlider.setMin(minInstant.toEpochMilli.doubleValue()) + highSlider.setMin(minInstant.toEpochMilli.doubleValue()) + lowSlider.setMax(maxInstant.toEpochMilli.doubleValue()) + highSlider.setMax(maxInstant.toEpochMilli.doubleValue()) + lowSlider.setValue(lowSlider.getMin) + highSlider.setValue(highSlider.getMax) + println("set" + minInstant + "/" + maxInstant) + case None => //println("none") + } + } + +} + + + diff --git a/app/src/main/scala/app/logorrr/views/ops/time/TimerSlider.scala b/app/src/main/scala/app/logorrr/views/ops/time/TimerSlider.scala new file mode 100644 index 00000000..d87c750f --- /dev/null +++ b/app/src/main/scala/app/logorrr/views/ops/time/TimerSlider.scala @@ -0,0 +1,57 @@ +package app.logorrr.views.ops.time + +import app.logorrr.model.LogEntry +import javafx.beans.binding.BooleanBinding +import javafx.collections.ObservableList +import javafx.scene.control.{Slider, Tooltip} + +import java.time.format.DateTimeFormatter +import java.time.{Instant, LocalDateTime, ZoneId} + +object TimerSlider { + + def format(epochMilli: Long, formatter: DateTimeFormatter): String = { + val instant = Instant.ofEpochMilli(epochMilli) + val dateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault) + dateTime.format(formatter) + } + + /** + * Given an observable list of log entries, calculate the min and max instant. LogEntries doesn't have to be ordered. + * + * @param logEntries list of log entries + * @return min and max Instant of all log entries + */ + def calculateBoundaries(logEntries: ObservableList[LogEntry]): Option[(Instant, Instant)] = { + if (!logEntries.isEmpty) { + var minInstant = Instant.MAX + var maxInstant = Instant.MIN + logEntries.forEach((e: LogEntry) => { + e.someInstant match { + case Some(instant) => + if (instant.isBefore(minInstant)) { + minInstant = instant + } + if (instant.isAfter(maxInstant)) { + maxInstant = instant + } + case None => // do nothing + } + }) + if (minInstant == Instant.MIN || minInstant == Instant.MAX) { + None + } else if (maxInstant == Instant.MIN || maxInstant == Instant.MAX) { + None + } else Option((minInstant, maxInstant)) + } else None + } + +} + +class TimerSlider(hasLogEntrySettingsBinding: BooleanBinding, tooltipText: String) extends Slider { + visibleProperty().bind(hasLogEntrySettingsBinding) + disableProperty().bind(hasLogEntrySettingsBinding.not) + setTooltip(new Tooltip(tooltipText)) + setPrefWidth(500) + +} \ No newline at end of file diff --git a/app/src/main/scala/app/logorrr/views/ops/time/TimestampSettingsButton.scala b/app/src/main/scala/app/logorrr/views/ops/time/TimestampSettingsButton.scala new file mode 100644 index 00000000..0cc0f52e --- /dev/null +++ b/app/src/main/scala/app/logorrr/views/ops/time/TimestampSettingsButton.scala @@ -0,0 +1,57 @@ +package app.logorrr.views.ops.time + +import app.logorrr.conf.mut.MutLogFileSettings +import app.logorrr.model.LogEntry +import app.logorrr.views.block.ChunkListView +import app.logorrr.views.settings.timestamp.TimestampSettingStage +import javafx.collections.ObservableList +import javafx.scene.control.{Button, Tooltip} +import javafx.scene.layout.StackPane +import org.kordamp.ikonli.fontawesome5.FontAwesomeRegular +import org.kordamp.ikonli.javafx.FontIcon + +/** + * Displays a clock in the ops tool bar, with a red exclamation mark if there is no setting for the + * timestamp format for the given log file. + * + * Given a time stamp format (for example: YYYY-MM-dd HH:mm:ss.SSS), LogoRRR is able to parse the timestamp + * and thus has new possibilities to analyse the log file. + * + * @param settings settings for specific log file + * @param logEntries the list of log entries to display in order to configure a time format + */ +class TimestampSettingsButton(settings: MutLogFileSettings + , chunkListView: ChunkListView + , logEntries: ObservableList[LogEntry] + , timeOpsToolBar: TimeOpsToolBar) extends StackPane { + + // since timerbutton is a stackpane, this css commands are necessary to have the same effect as + // defined in primer-light.css + val button: Button = { + val btn = new Button() + btn.setBackground(null) + btn.setStyle( + """ + |-color-button-bg: -color-bg-subtle; + |-fx-background-insets: 0; + |""".stripMargin) + btn.setGraphic(new FontIcon(FontAwesomeRegular.CLOCK)) + btn.setTooltip(new Tooltip("configure time format")) + btn.setOnAction(_ => new TimestampSettingStage(getScene.getWindow, settings, chunkListView, logEntries, timeOpsToolBar).showAndWait()) + btn + } + + private val fontIcon = { + val icon = new FontIcon() + icon.setStyle("-fx-icon-code:fas-exclamation-circle;-fx-icon-color:rgba(255, 0, 0, 1);-fx-icon-size:8;") + icon.setTranslateX(10) + icon.setTranslateY(-10) + + /** red exclamation mark is only visible if there is no timestamp setting for a given log file */ + icon.visibleProperty().bind(settings.hasLogEntrySettingBinding.not()) + icon + } + + getChildren.addAll(button, fontIcon) + +} diff --git a/app/src/main/scala/app/logorrr/views/ops/time/TimestampSliderLabel.scala b/app/src/main/scala/app/logorrr/views/ops/time/TimestampSliderLabel.scala new file mode 100644 index 00000000..0a0ba2a0 --- /dev/null +++ b/app/src/main/scala/app/logorrr/views/ops/time/TimestampSliderLabel.scala @@ -0,0 +1,14 @@ +package app.logorrr.views.ops.time + +import app.logorrr.conf.mut.MutLogFileSettings +import javafx.beans.binding.Bindings +import javafx.scene.control.{Label, Slider} + +class TimestampSliderLabel(mutLogFileSettings: MutLogFileSettings + , slider: Slider) extends Label { + setPrefWidth(200) + textProperty().bind(Bindings.createStringBinding(() => { + TimerSlider.format(slider.getValue.longValue(), mutLogFileSettings.getDateTimeFormatter) + }, slider.valueProperty)) + visibleProperty().bind(mutLogFileSettings.hasLogEntrySettingBinding) +} diff --git a/app/src/main/scala/app/logorrr/views/search/FilterButton.scala b/app/src/main/scala/app/logorrr/views/search/FilterButton.scala index e87572bf..98dbed07 100644 --- a/app/src/main/scala/app/logorrr/views/search/FilterButton.scala +++ b/app/src/main/scala/app/logorrr/views/search/FilterButton.scala @@ -18,7 +18,7 @@ object FilterButton extends UiNodeFilterAware { class FilterButton(val fileId: FileId , val filter: Filter , i: Int - , updateActiveFilter: () => Unit + , updateActiveFilter: => Unit , removeFilter: Filter => Unit) extends ToggleButton(filter.pattern) with CanLog { setId(FilterButton.uiNode(fileId, filter).value) @@ -29,23 +29,14 @@ class FilterButton(val fileId: FileId setGraphic(new RemoveFilterbutton(fileId, filter, removeFilter)) } setSelected(filter.active) -// logTrace(s"Fb: ${fileId.fileName} / ${filter.pattern}: setting selected to ${filter.active}") selectedProperty().addListener(new InvalidationListener { // if any of the buttons changes its selected value, reevaluate predicate // and thus change contents of all views which display filtered List - override def invalidated(observable: Observable): Unit = { - updateActiveFilter() - } - }) -/* - selectedProperty().addListener(new ChangeListener[lang.Boolean] { - override def changed(observableValue: ObservableValue[_ <: lang.Boolean], t: lang.Boolean, t1: lang.Boolean): Unit = { - val msg = s"Fb: ${fileId.fileName} / ${filter.pattern} Change from ${t} to ${t1}" - logTrace(msg) - } + override def invalidated(observable: Observable): Unit = updateActiveFilter + }) -*/ + def isUnclassified: Boolean = filter.isInstanceOf[UnclassifiedFilter] } diff --git a/app/src/main/scala/app/logorrr/views/search/FiltersToolBar.scala b/app/src/main/scala/app/logorrr/views/search/FiltersToolBar.scala index 078f62bc..f7b24f97 100644 --- a/app/src/main/scala/app/logorrr/views/search/FiltersToolBar.scala +++ b/app/src/main/scala/app/logorrr/views/search/FiltersToolBar.scala @@ -1,6 +1,6 @@ package app.logorrr.views.search -import app.logorrr.io.FileId +import app.logorrr.conf.mut.MutLogFileSettings import app.logorrr.model.LogEntry import app.logorrr.util.JfxUtils import javafx.beans.property.SimpleListProperty @@ -28,14 +28,10 @@ object FiltersToolBar { * * @param filteredList list of entries which are displayed (can be filtered via buttons) */ -class FiltersToolBar(fileId: FileId +class FiltersToolBar(mutLogFileSettings: MutLogFileSettings , filteredList: FilteredList[LogEntry] , removeFilter: Filter => Unit) extends ToolBar { - var filterButtons: Map[Filter, FilterButton] = Map[Filter, FilterButton]() - - var someUnclassifiedFilter: Option[(Filter, FilterButton)] = None - var occurrences: Map[Filter, Int] = Map().withDefaultValue(0) /** will be bound to the current active filter list */ @@ -67,28 +63,28 @@ class FiltersToolBar(fileId: FileId } private def updateUnclassified(): Unit = { - val unclassified = UnclassifiedFilter(filterButtons.keySet) + val unclassified = UnclassifiedFilter(mutLogFileSettings.filterButtons.keySet) updateOccurrences(unclassified) - val filterButton = new FilterButton(fileId, unclassified, occurrences(unclassified), updateActiveFilter, removeFilter) - someUnclassifiedFilter.foreach(ftb => getItems.remove(ftb._2)) + val filterButton = new FilterButton(mutLogFileSettings.getFileId, unclassified, occurrences(unclassified), mutLogFileSettings.updateActiveFilter(filteredList), removeFilter) + mutLogFileSettings.someUnclassifiedFilter.foreach(ftb => getItems.remove(ftb._2)) getItems.add(0, filterButton) - someUnclassifiedFilter = Option((unclassified, filterButton)) - updateActiveFilter() + mutLogFileSettings.someUnclassifiedFilter = Option((unclassified, filterButton)) + mutLogFileSettings.updateActiveFilter(filteredList) } private def addFilterButton(filter: Filter): Unit = { updateOccurrences(filter) - val filterButton = new FilterButton(fileId, filter, occurrences(filter), updateActiveFilter, removeFilter) + val filterButton = new FilterButton(mutLogFileSettings.getFileId, filter, occurrences(filter), mutLogFileSettings.updateActiveFilter(filteredList), removeFilter) filter.bind(filterButton) getItems.add(filterButton) - filterButtons = filterButtons.updated(filter, filterButton) + mutLogFileSettings.filterButtons = mutLogFileSettings.filterButtons.updated(filter, filterButton) } private def removeFilterButton(filter: Filter): Unit = { - val button = filterButtons(filter) + val button = mutLogFileSettings.filterButtons(filter) filter.unbind() getItems.remove(button) - filterButtons = filterButtons.removed(filter) + mutLogFileSettings.filterButtons = mutLogFileSettings.filterButtons.removed(filter) } def activeFilters(): Seq[Filter] = { @@ -102,22 +98,6 @@ class FiltersToolBar(fileId: FileId }).flatten.toSeq } - /** - * Filters are only active if selected. - * - * UnclassifiedFilter gets an extra handling since it depends on other filters - * - * @return - */ - def computeCurrentFilter(): Fltr = { - new AnyFilter(someUnclassifiedFilter.map(fst => if (fst._2.isSelected) Set(fst._1) else Set()).getOrElse(Set()) ++ - filterButtons.filter(fst => fst._2.isSelected).keySet) - } - - def updateActiveFilter(): Unit = { - filteredList.setPredicate((entry: LogEntry) => computeCurrentFilter().matches(entry.value)) - } - } diff --git a/app/src/main/scala/app/logorrr/views/search/OpsToolBar.scala b/app/src/main/scala/app/logorrr/views/search/OpsToolBar.scala index 838c33bd..2e531f3e 100644 --- a/app/src/main/scala/app/logorrr/views/search/OpsToolBar.scala +++ b/app/src/main/scala/app/logorrr/views/search/OpsToolBar.scala @@ -17,8 +17,6 @@ import javafx.scene.input.{KeyCode, KeyEvent} object OpsToolBar { - - /** increment / decrement font size */ val fontSizeStep: Int = 1 @@ -63,9 +61,6 @@ class OpsToolBar(fileId: FileId private val copySelectionButton = new CopyLogButton(fileId, filteredList) - // val firstNEntries: ObservableList[LogEntry] = TimerSettingsLogView.mkEntriesToShow(logEntries) - - // val timerButton = new TimerButton(fileId, firstNEntries) def execSearchOnHitEnter(event: KeyEvent): Unit = { if (event.getCode == KeyCode.ENTER) { diff --git a/app/src/main/scala/app/logorrr/views/search/TimerButton.scala b/app/src/main/scala/app/logorrr/views/search/TimerButton.scala deleted file mode 100644 index a29edd45..00000000 --- a/app/src/main/scala/app/logorrr/views/search/TimerButton.scala +++ /dev/null @@ -1,55 +0,0 @@ -package app.logorrr.views.search - -import app.logorrr.conf.LogoRRRGlobals -import app.logorrr.conf.mut.MutLogFileSettings -import app.logorrr.io.FileId -import app.logorrr.model.{LogEntry, LogEntryInstantFormat} -import app.logorrr.util.CanLog -import app.logorrr.views.settings.timer.TimerSettingStage -import javafx.beans.binding.BooleanBinding -import javafx.collections.ObservableList -import javafx.scene.control.{Button, Tooltip} -import javafx.scene.layout.StackPane -import org.kordamp.ikonli.fontawesome5.FontAwesomeRegular -import org.kordamp.ikonli.javafx.FontIcon - - -class TimerButton(fileId: FileId - , logEntriesToDisplay: ObservableList[LogEntry]) - extends StackPane - with CanLog { - - def getSettings: MutLogFileSettings = LogoRRRGlobals.getLogFileSettings(fileId) - - def updateLogEntrySetting(leif: LogEntryInstantFormat): Unit = { - getSettings.setLogEntryInstantFormat(leif) - LogoRRRGlobals.persist() - } - - val button = new Button() - button.setBackground(null) - // since timerbutton is a stackpane, this css commands are necessary to have the same effect as - // defined in primer-light.css - button.setStyle( - """ - |-color-button-bg: -color-bg-subtle; - |-fx-background-insets: 0; - |""".stripMargin) - - button.setGraphic(new FontIcon(FontAwesomeRegular.CLOCK)) - button.setTooltip(new Tooltip("configure time format")) - - button.setOnAction(_ => { - new TimerSettingStage(getSettings, updateLogEntrySetting, logEntriesToDisplay).showAndWait() - }) - - private val binding: BooleanBinding = LogoRRRGlobals.getLogFileSettings(fileId).hasLogEntrySettingBinding.not() - - private val icon = new FontIcon() - icon.setStyle("-fx-icon-code:fas-exclamation-circle;-fx-icon-color:rgba(255, 0, 0, 1);-fx-icon-size:8;") - icon.setTranslateX(10) - icon.setTranslateY(-10) - icon.visibleProperty().bind(binding) - - getChildren.addAll(button, icon) -} diff --git a/app/src/main/scala/app/logorrr/views/settings/timer/TimerSettingStage.scala b/app/src/main/scala/app/logorrr/views/settings/timer/TimerSettingStage.scala deleted file mode 100644 index 32b7344c..00000000 --- a/app/src/main/scala/app/logorrr/views/settings/timer/TimerSettingStage.scala +++ /dev/null @@ -1,27 +0,0 @@ -package app.logorrr.views.settings.timer - -import app.logorrr.conf.mut.MutLogFileSettings -import app.logorrr.model.{LogEntry, LogEntryInstantFormat} -import app.logorrr.util.JfxUtils -import javafx.collections.ObservableList -import javafx.scene.Scene -import javafx.stage.{Modality, Stage} - -object TimerSettingStage { - - val width = 1100 - val height = 300 - -} - -class TimerSettingStage(settings: MutLogFileSettings - , updateLogEntrySetting: LogEntryInstantFormat => Unit - , logEntriesToDisplay: ObservableList[LogEntry]) extends Stage { - - private val timerSettingsBorderPane = new TimerSettingsBorderPane(settings, logEntriesToDisplay, updateLogEntrySetting, JfxUtils.closeStage(this)) - val scene = new Scene(timerSettingsBorderPane, TimerSettingStage.width, TimerSettingStage.height) - initModality(Modality.APPLICATION_MODAL) - setTitle(s"Timer settings for ${settings.getFileId.fileName}") - setScene(scene) - setOnCloseRequest(_ => this.close()) -} diff --git a/app/src/main/scala/app/logorrr/views/settings/timer/TimerSettingsBorderPane.scala b/app/src/main/scala/app/logorrr/views/settings/timer/TimerSettingsBorderPane.scala deleted file mode 100644 index 98b12f72..00000000 --- a/app/src/main/scala/app/logorrr/views/settings/timer/TimerSettingsBorderPane.scala +++ /dev/null @@ -1,122 +0,0 @@ -package app.logorrr.views.settings.timer - -import app.logorrr.conf.mut.MutLogFileSettings -import app.logorrr.model.{LogEntry, LogEntryInstantFormat} -import app.logorrr.util.{CanLog, HLink} -import app.logorrr.views.UiNodes -import javafx.beans.binding.Bindings -import javafx.beans.property.SimpleObjectProperty -import javafx.collections.ObservableList -import javafx.scene.control._ -import javafx.scene.layout.{BorderPane, VBox} - - -object TimerSettingsBorderPane { - - - private val dateTimeFormatterLink: HLink = HLink(UiNodes.OpenDateFormatterSite,"https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html", "format description") - - def mkTf(name: String - , somePrompt: Option[String] - , someDefault: Option[String] - , columnCount: Int): (Label, TextField) = { - val l = new Label(name) - val tf = new TextField() - someDefault.foreach(df => tf.setText(df)) - somePrompt.foreach(pt => tf.setPromptText(pt)) - tf.setPrefColumnCount(columnCount) - (l, tf) - } -} - -class TimerSettingsBorderPane(settings: MutLogFileSettings - , logEntriesToDisplay: ObservableList[LogEntry] - , updateLogEntrySetting: LogEntryInstantFormat => Unit - , closeStage: => Unit) - extends BorderPane with CanLog { - - /* - * those properties exist since it is easier to use from the call sites. - **/ - private val (startColProperty, endColProperty) = settings.someLogEntrySettingsProperty.get() match { - case Some(value) => (new SimpleObjectProperty[java.lang.Integer](value.startCol), new SimpleObjectProperty[java.lang.Integer](value.endCol)) - case None => (new SimpleObjectProperty[java.lang.Integer](), new SimpleObjectProperty[java.lang.Integer]()) - } - - private val rangeTextBinding = Bindings.createStringBinding(() => { - (Option(getStartCol), Option(getEndCol)) match { - case (Some(start), Some(end)) => s"Range: (${start.toString}/${end.toString})" - case (Some(start), None) => s"Range: (${start.toString}/not set)" - case (None, Some(end)) => s"select start col: (not set/${end.toString})" - case (None, None) => "Select range." - } - - }, startColProperty, endColProperty) - - private val rangeColLabel = { - val l = new Label() - l.textProperty().bind(rangeTextBinding) - l - } - - private val selectedRangeLabel = new Label("Selected: ") - private val (timeFormatLabel, timeFormatTf) = TimerSettingsBorderPane.mkTf("time format", Option(""), Option(LogEntryInstantFormat.DefaultPattern), 30) - - private val timerSettingsLogTextView = { - val tslv = new TimerSettingsLogView(settings, logEntriesToDisplay) - startColProperty.bind(tslv.startColProperty) - endColProperty.bind(tslv.endColProperty) - tslv - } - - /** - * if ok button is clicked, log definition will be written, settings stage will be closed, associated logfile - * definition will be updated - * */ - private val okButton = { - val b = new Button("set new format") - b.setOnAction(_ => { - updateLogEntrySetting(LogEntryInstantFormat(SimpleRange(getStartCol, getEndCol), timeFormatTf.getText.trim)) - closeStage - }) - b - } -/* - private val stringBinding = new StringBinding { - bind(startColProperty, endColProperty) - - override def computeValue(): String = { - "" - } - }*/ - - private val hyperlink: Hyperlink = TimerSettingsBorderPane.dateTimeFormatterLink.mkHyperLink() - private val selectedBar = new ToolBar(selectedRangeLabel) - private val timeFormatBar = new ToolBar(timeFormatLabel, timeFormatTf, hyperlink) - private val bar = new ToolBar(rangeColLabel, okButton) - private val vbox = new VBox(selectedBar, timeFormatBar, bar) - - init() - - def setStartCol(startCol: Int): Unit = startColProperty.set(startCol) - - def setEndCol(endCol: Int): Unit = endColProperty.set(endCol) - - def getStartCol: java.lang.Integer = startColProperty.get() - - def getEndCol: java.lang.Integer = endColProperty.get() - - - def init(): Unit = { - settings.someLogEntrySettingsProperty.get() match { - case Some(s) => - timeFormatTf.setText(s.dateTimePattern) - case None => - logTrace("No time setting found ... ") - } - - setCenter(timerSettingsLogTextView) - setBottom(vbox) - - } -} \ No newline at end of file diff --git a/app/src/main/scala/app/logorrr/views/settings/timer/SimpleRange.scala b/app/src/main/scala/app/logorrr/views/settings/timestamp/SimpleRange.scala similarity index 74% rename from app/src/main/scala/app/logorrr/views/settings/timer/SimpleRange.scala rename to app/src/main/scala/app/logorrr/views/settings/timestamp/SimpleRange.scala index 40e3bde1..98564bf2 100644 --- a/app/src/main/scala/app/logorrr/views/settings/timer/SimpleRange.scala +++ b/app/src/main/scala/app/logorrr/views/settings/timestamp/SimpleRange.scala @@ -1,4 +1,4 @@ -package app.logorrr.views.settings.timer +package app.logorrr.views.settings.timestamp import pureconfig.{ConfigReader, ConfigWriter} import pureconfig.generic.semiauto.{deriveReader, deriveWriter} @@ -11,5 +11,5 @@ object SimpleRange { } case class SimpleRange(start: Int, end: Int) { - require(start <= end) + require(start <= end, s"Expected start <= end, but was $start <= $end") } \ No newline at end of file diff --git a/app/src/main/scala/app/logorrr/views/settings/timer/TimerSettingsLogView.scala b/app/src/main/scala/app/logorrr/views/settings/timestamp/TimerSettingsLogView.scala similarity index 87% rename from app/src/main/scala/app/logorrr/views/settings/timer/TimerSettingsLogView.scala rename to app/src/main/scala/app/logorrr/views/settings/timestamp/TimerSettingsLogView.scala index 6e830e97..f6c94c07 100644 --- a/app/src/main/scala/app/logorrr/views/settings/timer/TimerSettingsLogView.scala +++ b/app/src/main/scala/app/logorrr/views/settings/timestamp/TimerSettingsLogView.scala @@ -1,4 +1,4 @@ -package app.logorrr.views.settings.timer +package app.logorrr.views.settings.timestamp import app.logorrr.conf.mut.MutLogFileSettings import app.logorrr.model.LogEntry @@ -18,13 +18,13 @@ object TimerSettingsLogView { } -class TimerSettingsLogView(settings: MutLogFileSettings +class TimerSettingsLogView(mutLogFileSettings: MutLogFileSettings , logEntries: ObservableList[LogEntry]) extends BorderPane { val startColProperty: ObjectProperty[java.lang.Integer] = new SimpleObjectProperty[java.lang.Integer](null) val endColProperty: ObjectProperty[java.lang.Integer] = new SimpleObjectProperty[java.lang.Integer](null) - settings.someLogEntrySettingsProperty.get() match { + mutLogFileSettings.getSomeTimestampSettings match { case Some(s) => setStartCol(s.startCol) setEndCol(s.endCol) @@ -50,7 +50,7 @@ class TimerSettingsLogView(settings: MutLogFileSettings def setEndCol(i: Int): Unit = endColProperty.set(i) class LogEntryListCell extends ListCell[LogEntry] { - styleProperty().bind(settings.fontStyleBinding) + styleProperty().bind(mutLogFileSettings.fontStyleBinding) setGraphic(null) override def updateItem(t: LogEntry, b: Boolean): Unit = { @@ -58,7 +58,7 @@ class TimerSettingsLogView(settings: MutLogFileSettings Option(t) match { case Some(e) => setText(null) - setGraphic(TimerSettingsLogViewLabel(settings + setGraphic(TimerSettingsLogViewLabel(mutLogFileSettings , e , maxLength , startColProperty diff --git a/app/src/main/scala/app/logorrr/views/settings/timer/TimerSettingsLogViewLabel.scala b/app/src/main/scala/app/logorrr/views/settings/timestamp/TimerSettingsLogViewLabel.scala similarity index 71% rename from app/src/main/scala/app/logorrr/views/settings/timer/TimerSettingsLogViewLabel.scala rename to app/src/main/scala/app/logorrr/views/settings/timestamp/TimerSettingsLogViewLabel.scala index 44cb1c91..2fe0cd1c 100644 --- a/app/src/main/scala/app/logorrr/views/settings/timer/TimerSettingsLogViewLabel.scala +++ b/app/src/main/scala/app/logorrr/views/settings/timestamp/TimerSettingsLogViewLabel.scala @@ -1,35 +1,25 @@ -package app.logorrr.views.settings.timer +package app.logorrr.views.settings.timestamp import app.logorrr.conf.mut.MutLogFileSettings import app.logorrr.model.LogEntry import app.logorrr.util.LabelUtil import app.logorrr.views.text.LineNumberLabel import javafx.beans.property.ObjectProperty -import javafx.event.EventHandler import javafx.scene.control.{Label, Tooltip} import javafx.scene.input.MouseEvent import javafx.scene.layout.{Background, BackgroundFill, HBox} import javafx.scene.paint.Color object TimerSettingsLogViewLabel { -/* - def calcStyle(startColProperty: ObjectProperty[java.lang.Integer] - , endColProperty: ObjectProperty[java.lang.Integer]): Option[String] = { - (Option(startColProperty.get()), Option(endColProperty.get)) match { - case (Some(_), Some(_)) => - Some("") - case _ => None - } - } - */ } case class TimerSettingsLogViewLabel(settings: MutLogFileSettings , e: LogEntry , maxLength: Int , startColProperty: ObjectProperty[java.lang.Integer] - , endColProperty: ObjectProperty[java.lang.Integer]) extends HBox { + , endColProperty: ObjectProperty[java.lang.Integer]) + extends HBox { val lineNumberLabel: LineNumberLabel = LineNumberLabel(e.lineNumber, maxLength) lineNumberLabel.styleProperty().bind(settings.fontStyleBinding) @@ -37,22 +27,15 @@ case class TimerSettingsLogViewLabel(settings: MutLogFileSettings for ((c, i) <- e.value.zipWithIndex) yield { val l = new Label(c.toString) l.setUserData(i) // save position of label for later - l.setOnMouseClicked(new EventHandler[MouseEvent] { - override def handle(t: MouseEvent): Unit = { - applyStyleAtPos(i) - } - }) -// l.setOnMouseClicked(applyStyleAtPos(i) _) + l.setOnMouseClicked((_: MouseEvent) => applyStyleAtPos(i)) l.setTooltip(new Tooltip(s"column: ${i.toString}")) l.setOnMouseEntered(_ => { l.setStyle( """-fx-border-color: RED; - |-fx-border-width: 0 0 0 1px; + |-fx-border-width: 0 0 0 3px; |""".stripMargin) }) - l.setOnMouseExited(_ => { - l.setStyle("") - }) + l.setOnMouseExited(_ => l.setStyle("")) l } @@ -63,12 +46,27 @@ case class TimerSettingsLogViewLabel(settings: MutLogFileSettings private def applyStyleAtPos(pos: Int): Unit = { (Option(startColProperty.get), Option(endColProperty.get)) match { + // if none is set, start with startcol case (None, None) => startColProperty.set(pos) chars.foreach(LabelUtil.resetStyle) + // if both are set, start with startcol again + case (Some(_), Some(_)) => + startColProperty.set(pos) + endColProperty.set(null) + chars.foreach(LabelUtil.resetStyle) + // startcol is set already, set endcol case (Some(startCol), None) => - endColProperty.set(pos) - paint(startCol, pos) + // if startCol is greater or equal to pos, intpret it as new startcol + if (startCol >= pos) { + startColProperty.set(pos) + endColProperty.set(null) + chars.foreach(LabelUtil.resetStyle) + } else { + endColProperty.set(pos) + paint(startCol, pos) + } + // not reachable (?) case _ => chars.foreach(LabelUtil.resetStyle) startColProperty.set(null) @@ -80,7 +78,7 @@ case class TimerSettingsLogViewLabel(settings: MutLogFileSettings for (l <- chars) { val labelIndex = l.getUserData.asInstanceOf[Int] if (startCol <= labelIndex && labelIndex < endCol) { - l.setBackground(new Background(new BackgroundFill(Color.LIGHTGRAY, null, null))) + l.setBackground(new Background(new BackgroundFill(Color.YELLOWGREEN, null, null))) } } } diff --git a/app/src/main/scala/app/logorrr/views/settings/timestamp/TimestampSettingStage.scala b/app/src/main/scala/app/logorrr/views/settings/timestamp/TimestampSettingStage.scala new file mode 100644 index 00000000..68377a21 --- /dev/null +++ b/app/src/main/scala/app/logorrr/views/settings/timestamp/TimestampSettingStage.scala @@ -0,0 +1,49 @@ +package app.logorrr.views.settings.timestamp + +import app.logorrr.conf.mut.MutLogFileSettings +import app.logorrr.model.LogEntry +import app.logorrr.util.JfxUtils +import app.logorrr.views.block.ChunkListView +import app.logorrr.views.ops.time.TimeOpsToolBar +import javafx.collections.ObservableList +import javafx.scene.Scene +import javafx.stage.{Modality, Stage, Window} + +object TimestampSettingStage { + + val width = 1100 + val height = 300 + +} + +class TimestampSettingStage(owner: Window + , settings: MutLogFileSettings + , chunkListView: ChunkListView + , logEntries: ObservableList[LogEntry] + , timeOpsToolBar: TimeOpsToolBar) extends Stage { + + initOwner(owner) + initModality(Modality.APPLICATION_MODAL) + setTitle(s"Specify the timestamp range (from - to columns) and the time pattern for ${settings.getFileId.fileName}") + + // center relative to owner window + /* + setOnShowing(_ => { + val x = owner.getX + (owner.getWidth - getWidth) / 2 + val y = owner.getY + (owner.getHeight - getHeight) / 2 + setX(x) + setY(y) + }) + */ + + val scene = new Scene( + new TimestampSettingsBorderPane(settings + , logEntries + , chunkListView + , timeOpsToolBar + , JfxUtils.closeStage(this)) + , TimestampSettingStage.width + , TimestampSettingStage.height) + setScene(scene) + setOnCloseRequest(_ => this.close()) +} diff --git a/app/src/main/scala/app/logorrr/views/settings/timestamp/TimestampSettingsBorderPane.scala b/app/src/main/scala/app/logorrr/views/settings/timestamp/TimestampSettingsBorderPane.scala new file mode 100644 index 00000000..978a9e54 --- /dev/null +++ b/app/src/main/scala/app/logorrr/views/settings/timestamp/TimestampSettingsBorderPane.scala @@ -0,0 +1,220 @@ +package app.logorrr.views.settings.timestamp + +import app.logorrr.conf.LogoRRRGlobals +import app.logorrr.conf.mut.MutLogFileSettings +import app.logorrr.model.{LogEntry, TimestampSettings} +import app.logorrr.util.{CanLog, HLink, JfxUtils} +import app.logorrr.views.UiNodes +import app.logorrr.views.block.ChunkListView +import app.logorrr.views.ops.time.TimeOpsToolBar +import javafx.beans.binding.{Bindings, ObjectBinding} +import javafx.beans.property.SimpleObjectProperty +import javafx.collections.ObservableList +import javafx.geometry.{Insets, Pos} +import javafx.scene.control._ +import javafx.scene.layout.{BorderPane, HBox, Region, VBox} + +import java.time.{Duration, Instant} +import java.util + + +object TimestampSettingsBorderPane { + + +} + +class TimestampSettingsBorderPane(mutLogFileSettings: MutLogFileSettings + , logEntries: ObservableList[LogEntry] + , chunkListView: ChunkListView + , timeOpsToolBar: TimeOpsToolBar + , closeStage: => Unit) + extends BorderPane with CanLog { + + val labelWidth = 100 + val textFieldWidth = 60 + + private val fromTextField = { + val tf = JfxUtils.mkTextField(textFieldWidth) + tf.setEditable(false) + tf + } + + private val toTextField = { + val tf = JfxUtils.mkTextField(textFieldWidth) + tf.setEditable(false) + tf + } + + private val fromLabel = JfxUtils.mkL("from column:", labelWidth) + private val toLabel = JfxUtils.mkL("to column:", labelWidth) + + + /* + * those properties exist since it is easier to use from the call sites. + **/ + private val (startColProperty, endColProperty) = mutLogFileSettings.getSomeTimestampSettings match { + case Some(value) => (new SimpleObjectProperty[java.lang.Integer](value.startCol), new SimpleObjectProperty[java.lang.Integer](value.endCol)) + case None => (new SimpleObjectProperty[java.lang.Integer](), new SimpleObjectProperty[java.lang.Integer]()) + } + + fromTextField.textProperty().bind(Bindings.createStringBinding(() => { + Option(getStartCol) match { + case Some(value) => value.toString + case None => "" + } + }, startColProperty)) + + toTextField.textProperty().bind(Bindings.createStringBinding(() => { + Option(getEndCol) match { + case Some(value) => value.toString + case None => "" + } + }, endColProperty)) + + + private val resetButton = { + val b = new Button("reset") + b.setAlignment(Pos.CENTER_RIGHT) + b.setPrefWidth(180) + b.setOnAction(_ => { + mutLogFileSettings.setSomeLogEntryInstantFormat(None) + LogoRRRGlobals.persist() + // we have to deactivate this listener otherwise + chunkListView.removeInvalidationListener() + val tempList = new util.ArrayList[LogEntry]() + logEntries.forEach(e => { + tempList.add(e.withOutTimestamp()) + }) + logEntries.setAll(tempList) + // activate listener again + chunkListView.addInvalidationListener() + timeOpsToolBar.updateSliderBoundaries() + closeStage + }) + b + } + + /** + * if ok button is clicked, log definition will be written, settings stage will be closed, associated logfile + * definition will be updated + * */ + private val okButton = { + val b = new Button("set format") + b.setPrefWidth(400) + b.setAlignment(Pos.CENTER) + b.setOnAction(_ => { + val leif: TimestampSettings = TimestampSettings(SimpleRange(getStartCol, getEndCol), timeFormatTf.getText.trim) + mutLogFileSettings.setSomeLogEntryInstantFormat(Option(leif)) + LogoRRRGlobals.persist() + // we have to deactivate this listener otherwise + chunkListView.removeInvalidationListener() + var someFirstEntryTimestamp: Option[Instant] = None + + val tempList = new util.ArrayList[LogEntry]() + for (i <- 0 until logEntries.size()) { + val e = logEntries.get(i) + val someInstant = TimestampSettings.parseInstant(e.value, leif) + if (someFirstEntryTimestamp.isEmpty) { + someFirstEntryTimestamp = someInstant + } + + val diffFromStart: Option[Duration] = for { + firstEntry <- someFirstEntryTimestamp + instant <- someInstant + } yield Duration.between(firstEntry, instant) + + tempList.add(e.copy(someInstant = someInstant, someDurationSinceFirstInstant = diffFromStart)) + } + logEntries.setAll(tempList) + // activate listener again + chunkListView.addInvalidationListener() + // update slider boundaries + timeOpsToolBar.updateSliderBoundaries() + closeStage + }) + b + } + + // has to be assigned to a val otherwise this won't get intepreted + val binding: ObjectBinding[String] = + Bindings.createObjectBinding(() => s"$getStartCol $getEndCol", startColProperty, endColProperty) + + // if either startCol or endCol is changed, refresh listview + binding.addListener((_, _, _) => { + timerSettingsLogTextView.listView.refresh() + }) + + private val hyperlink: Hyperlink = { + val hl = HLink(UiNodes.OpenDateFormatterSite, "https://docs.oracle.com/en/java/javase/22/docs/api/java.base/java/time/format/DateTimeFormatter.html", "time pattern").mkHyperLink() + hl.setAlignment(Pos.CENTER) + hl.setPrefWidth(83) + hl + } + + private val timeFormatTf = { + val tf = new TextField() + tf.setPromptText("") + tf.setText(TimestampSettings.DefaultPattern) + tf.setPrefColumnCount(30) + tf + } + + private val timerSettingsLogTextView = { + val tslv = new TimerSettingsLogView(mutLogFileSettings, logEntries) + startColProperty.bind(tslv.startColProperty) + endColProperty.bind(tslv.endColProperty) + tslv + } + + private val spacer = { + val s = new Region() + HBox.setHgrow(s, javafx.scene.layout.Priority.ALWAYS) + s + } + private val timeFormatBar = new ToolBar(hyperlink, timeFormatTf, okButton, spacer, resetButton) + + init() + + + def init(): Unit = { + mutLogFileSettings.getSomeTimestampSettings match { + case Some(s) => timeFormatTf.setText(s.dateTimePattern) + case None => logTrace("No time setting found ... ") + } + + val leftLabel = new Label("select range") + val leftVBox = new VBox(leftLabel) + leftVBox.setAlignment(Pos.CENTER); // Center the label vertically + leftVBox.setPadding(new Insets(10)) + setLeft(leftVBox) + + val fromRow = new HBox(10, fromLabel, fromTextField) // Spacing between label and text field is 10 + fromRow.setAlignment(Pos.CENTER_LEFT) + + val toRow = new HBox(10, toLabel, toTextField) // Spacing between label and text field is 10 + + toRow.setAlignment(Pos.CENTER_LEFT) + + val vbox = new VBox(10, fromRow, toRow) // Spacing between rows is 10 + + vbox.setAlignment(Pos.CENTER) // Center the elements in the VBox + + vbox.setPadding(new Insets(10)) // Padding around the VBox + + + setRight(vbox) + + setCenter(timerSettingsLogTextView) + setBottom(timeFormatBar) + + } + + def setStartCol(startCol: Int): Unit = startColProperty.set(startCol) + + def setEndCol(endCol: Int): Unit = endColProperty.set(endCol) + + def getStartCol: java.lang.Integer = startColProperty.get() + + def getEndCol: java.lang.Integer = endColProperty.get() + +} \ No newline at end of file diff --git a/app/src/main/scala/app/logorrr/views/text/LineTimerLabel.scala b/app/src/main/scala/app/logorrr/views/text/LineTimerLabel.scala index 195f2a63..e3fbf32e 100644 --- a/app/src/main/scala/app/logorrr/views/text/LineTimerLabel.scala +++ b/app/src/main/scala/app/logorrr/views/text/LineTimerLabel.scala @@ -3,13 +3,13 @@ package app.logorrr.views.text import org.kordamp.ikonli.fontawesome5.FontAwesomeSolid import org.kordamp.ikonli.javafx.FontIcon -import java.time.Instant +import java.time.Duration object LineTimerLabel { - def apply(instant: Instant): LineTimerLabel = { + def apply(duration: Duration): LineTimerLabel = { val ldl = new LineTimerLabel - ldl.setText(instant.toString) + ldl.setText(duration.toMillis.toString) ldl } } diff --git a/app/src/main/scala/app/logorrr/views/text/LogTextView.scala b/app/src/main/scala/app/logorrr/views/text/LogTextView.scala index 5becd4d6..892d0531 100644 --- a/app/src/main/scala/app/logorrr/views/text/LogTextView.scala +++ b/app/src/main/scala/app/logorrr/views/text/LogTextView.scala @@ -36,9 +36,8 @@ class LogTextView(mutLogFileSettings: MutLogFileSettings }) // when changing font size, repaint - private lazy val refreshListener = JfxUtils.onNew[Number](_ => { - refresh() // otherwise listview is not repainted correctly since calculation of the cellheight is broken atm - }) + // otherwise listview is not repainted correctly since calculation of the cellheight is broken atm + private lazy val refreshListener = JfxUtils.onNew[Number](_ => refresh()) private lazy val scrollBarListener = JfxUtils.onNew[Number](_ => { val (first, last) = ListViewHelper.getVisibleRange(this) @@ -90,7 +89,6 @@ class LogTextView(mutLogFileSettings: MutLogFileSettings getSelectionModel.selectedItemProperty().addListener(selectedLineNumberListener) mutLogFileSettings.fontSizeProperty.addListener(refreshListener) - skinProperty.addListener(skinListener) setItems(filteredList) diff --git a/app/src/main/scala/app/logorrr/views/text/LogTextViewLabel.scala b/app/src/main/scala/app/logorrr/views/text/LogTextViewLabel.scala index 4e291de9..8dc05fcf 100644 --- a/app/src/main/scala/app/logorrr/views/text/LogTextViewLabel.scala +++ b/app/src/main/scala/app/logorrr/views/text/LogTextViewLabel.scala @@ -43,11 +43,13 @@ case class LogTextViewLabel(e: LogEntry l } - e.someInstant.foreach( - i => getChildren.add(LineTimerLabel(i)) - ) getChildren.add(lineNumberLabel) + /* + e.someDurationSinceFirstInstant.foreach( + duration => getChildren.add(LineTimerLabel(duration)) + ) + */ getChildren.addAll(labels: _*) } diff --git a/app/src/test/scala/app/logorrr/LogEntrySpec.scala b/app/src/test/scala/app/logorrr/LogEntrySpec.scala index 7dab16ff..760a9617 100644 --- a/app/src/test/scala/app/logorrr/LogEntrySpec.scala +++ b/app/src/test/scala/app/logorrr/LogEntrySpec.scala @@ -14,7 +14,8 @@ object LogEntrySpec { l <- Gen.posNum[Int] value <- Gen.alphaStr someInstant <- Gen.const(None) - } yield LogEntry(l, value, someInstant) + someDuration <- Gen.const(None) + } yield LogEntry(l, value, someInstant, someDuration) } class LogEntrySpec extends AnyWordSpecLike { diff --git a/app/src/test/scala/app/logorrr/conf/mut/LogEntryInstantFormatSpec.scala b/app/src/test/scala/app/logorrr/conf/mut/LogEntryInstantFormatSpec.scala deleted file mode 100644 index 18480c3c..00000000 --- a/app/src/test/scala/app/logorrr/conf/mut/LogEntryInstantFormatSpec.scala +++ /dev/null @@ -1,12 +0,0 @@ -package app.logorrr.conf.mut - -import app.logorrr.model.LogEntryInstantFormat -import org.scalacheck.Gen - -object LogEntryInstantFormatSpec { - val gen: Gen[LogEntryInstantFormat] = for { - sr <- SimpleRangeSpec.gen - dtp <- Gen.const(LogEntryInstantFormat.DefaultPattern) - zo <- Gen.const("+1") - } yield LogEntryInstantFormat(sr, dtp, zo) -} diff --git a/app/src/test/scala/app/logorrr/conf/mut/LogFileSettingsSpec.scala b/app/src/test/scala/app/logorrr/conf/mut/LogFileSettingsSpec.scala index d30d972b..6fec4d27 100644 --- a/app/src/test/scala/app/logorrr/conf/mut/LogFileSettingsSpec.scala +++ b/app/src/test/scala/app/logorrr/conf/mut/LogFileSettingsSpec.scala @@ -2,7 +2,7 @@ package app.logorrr.conf.mut import app.logorrr.conf.CoreGen import app.logorrr.io.FileId -import app.logorrr.model.LogFileSettings +import app.logorrr.model.{LogFileSettings, TimestampSettingsSpec} import org.scalacheck.Gen @@ -14,7 +14,7 @@ object LogFileSettingsSpec { firstOpened <- Gen.posNum[Long] dPos <- Gen.posNum[Double] filters <- Gen.listOf(FilterSpec.gen) - leif <- LogEntryInstantFormatSpec.gen + leif <- TimestampSettingsSpec.gen someLogEntryInstantFormat <- Gen.oneOf(None, Option(leif)) blockSettings <- BlockSettingsSpec.gen fontSize <- Gen.posNum[Int] @@ -29,5 +29,7 @@ object LogFileSettingsSpec { , someLogEntryInstantFormat , autoScroll , 0 - , 10) + , 10 + , LogFileSettings.DefaultLowerTimestamp + , LogFileSettings.DefaultUpperTimestamp) } diff --git a/app/src/test/scala/app/logorrr/conf/mut/SimpleRangeSpec.scala b/app/src/test/scala/app/logorrr/conf/mut/SimpleRangeSpec.scala index 81274979..34585e49 100644 --- a/app/src/test/scala/app/logorrr/conf/mut/SimpleRangeSpec.scala +++ b/app/src/test/scala/app/logorrr/conf/mut/SimpleRangeSpec.scala @@ -1,6 +1,6 @@ package app.logorrr.conf.mut -import app.logorrr.views.settings.timer.SimpleRange +import app.logorrr.views.settings.timestamp.SimpleRange import org.scalacheck.Gen import scala.util.Random diff --git a/app/src/test/scala/app/logorrr/model/TimestampSettingsSpec.scala b/app/src/test/scala/app/logorrr/model/TimestampSettingsSpec.scala new file mode 100644 index 00000000..b879a7a7 --- /dev/null +++ b/app/src/test/scala/app/logorrr/model/TimestampSettingsSpec.scala @@ -0,0 +1,33 @@ +package app.logorrr.model + +import app.logorrr.conf.mut.SimpleRangeSpec +import app.logorrr.views.settings.timestamp.SimpleRange +import org.scalacheck.Gen +import org.scalatest.wordspec.AnyWordSpec + +import java.time.Instant +import scala.util.Try + +object TimestampSettingsSpec { + val gen: Gen[TimestampSettings] = for { + sr <- SimpleRangeSpec.gen + dtp <- Gen.const(TimestampSettings.DefaultPattern) + } yield TimestampSettings(sr, dtp) +} + + +class TimestampSettingsSpec extends AnyWordSpec { + + "Log Format Tests" should { + "parse valid timestamp" in { + val t: Option[Instant] = TimestampSettings.parseInstant("2024-08-11 12:38:00", TimestampSettings(SimpleRange(0, 19), "yyyy-MM-dd HH:mm:ss")) + assert(t.isDefined) + t.foreach(x => println(x.toEpochMilli)) + assert(t.exists(_.toEpochMilli == 1723372680000L)) + } + "Instant.MIN.toEpocMilli throws exception" in assert(Try(Instant.MIN.toEpochMilli).isFailure) + "Instant.MAX.toEpocMilli throws exception" in assert(Try(Instant.MAX.toEpochMilli).isFailure) + "compare Instant.MIN with itself" in assert(Instant.MIN == Instant.MIN && Instant.MIN.equals(Instant.MIN)) + "compare Instant.MAX with itself" in assert(Instant.MAX == Instant.MAX && Instant.MAX.equals(Instant.MAX)) + } +} diff --git a/app/src/test/scala/app/logorrr/views/block/ChunkSpec.scala b/app/src/test/scala/app/logorrr/views/block/ChunkSpec.scala index 596199c8..6aa219de 100644 --- a/app/src/test/scala/app/logorrr/views/block/ChunkSpec.scala +++ b/app/src/test/scala/app/logorrr/views/block/ChunkSpec.scala @@ -9,7 +9,7 @@ object ChunkSpec { def mkTestLogEntries(size: Int): java.util.List[LogEntry] = { val entries = { (0 until size).foldLeft(new java.util.ArrayList[LogEntry]())((acc, e) => { - acc.add(LogEntry(e, "", None)) + acc.add(LogEntry(e, "", None, None)) acc }) } diff --git a/app/src/test/scala/app/logorrr/views/ops/time/TimerSliderSpec.scala b/app/src/test/scala/app/logorrr/views/ops/time/TimerSliderSpec.scala new file mode 100644 index 00000000..18d738c5 --- /dev/null +++ b/app/src/test/scala/app/logorrr/views/ops/time/TimerSliderSpec.scala @@ -0,0 +1,37 @@ +package app.logorrr.views.ops.time + +import app.logorrr.model.LogEntry +import javafx.collections.FXCollections +import org.scalatest.wordspec.AnyWordSpec + +import java.time.{Duration, Instant} +import java.util.Collections + +class TimerSliderSpec extends AnyWordSpec { + + "TimerSlider.calculateBoundaries" should { + ".empty" in { + val l = FXCollections.observableList(Collections.emptyList[LogEntry]()) + assert(TimerSlider.calculateBoundaries(l).isEmpty) + } + ".one entry without timestamp" in { + val l = FXCollections.observableList(Collections.singletonList[LogEntry](LogEntry(0, "", None, None))) + assert(TimerSlider.calculateBoundaries(l).isEmpty) + } + ".more entries without timestamp" in { + val list = List.fill(100)(LogEntry(0, "", None, None)) + val l = FXCollections.observableArrayList(list: _*) + assert(TimerSlider.calculateBoundaries(l).isEmpty) + } + ".one entry with timestamp" in { + val l = FXCollections.observableList(Collections.singletonList[LogEntry](LogEntry(0, "", Option(Instant.now), Option(Duration.ofSeconds(0))))) + assert(TimerSlider.calculateBoundaries(l).isDefined) + } + ".two entries with timestamp" in { + val l = FXCollections.observableArrayList( + LogEntry(0, "", Option(Instant.now), Option(Duration.ofSeconds(0))), LogEntry(1, "", Option(Instant.now.plusSeconds(10)), Option(Duration.ofSeconds(10)))) + assert(TimerSlider.calculateBoundaries(l).isDefined) + } + + } +} diff --git a/app/src/test/scala/app/logorrr/views/text/FilterCalculatorSpec.scala b/app/src/test/scala/app/logorrr/views/text/FilterCalculatorSpec.scala index eeb12687..bc419ec7 100644 --- a/app/src/test/scala/app/logorrr/views/text/FilterCalculatorSpec.scala +++ b/app/src/test/scala/app/logorrr/views/text/FilterCalculatorSpec.scala @@ -21,7 +21,7 @@ object FilterCalculatorSpec { class FilterCalculatorSpec extends LogoRRRSpec { def applySingleFilter(logEntry: String, pattern: String): Seq[Seq[LinePart]] = { - FilterCalculator(LogEntry(0,logEntry, None), Seq(new Filter(pattern, Color.RED, true))).filteredParts + FilterCalculator(LogEntry(0, logEntry, None, None), Seq(new Filter(pattern, Color.RED, true))).filteredParts } "calcParts" should { @@ -85,7 +85,7 @@ class FilterCalculatorSpec extends LogoRRRSpec { , new Filter("b", Color.BLUE, true) , new Filter("t", Color.YELLOW, true) ) - val entry = LogEntry(0, "test a b c", None) + val entry = LogEntry(0, "test a b c", None, None) val calculator = FilterCalculator(entry, filters) "produce correct amount of matches" in { diff --git a/core/src/main/scala/app/logorrr/util/CanLog.scala b/core/src/main/scala/app/logorrr/util/CanLog.scala index 9a06be0b..4dd96140 100644 --- a/core/src/main/scala/app/logorrr/util/CanLog.scala +++ b/core/src/main/scala/app/logorrr/util/CanLog.scala @@ -16,7 +16,7 @@ object CanLog { val RN = s"$R$N" /* before deploying make sure this is set to Level.INFO */ - val LogLevel = Level.INFO + val LogLevel: Level = Level.INFO /** if set to true, the LogoRRRs log file grows without boundaries */ val appendLogs = false @@ -37,7 +37,7 @@ object CanLog { h } - def throwToString(t: Throwable): String = { + private def throwToString(t: Throwable): String = { val sw = new StringWriter val pw = new PrintWriter(sw) try {