From 8bb945088ae66f9687f1fd5aa72a1440aba54466 Mon Sep 17 00:00:00 2001 From: PHO Date: Tue, 30 Sep 2014 02:29:34 +0900 Subject: [PATCH] wip --- build.sbt | 3 + .../scala/jp/ymir/taskReporter/Main.scala | 4 +- .../jp/ymir/taskReporter/Preferences.scala | 31 +++++++ .../jp/ymir/taskReporter/core/Report.scala | 14 +++ .../jp/ymir/taskReporter/core/ReportSet.scala | 36 ++++++-- .../jp/ymir/taskReporter/core/Task.scala | 48 ++++++++++ .../jp/ymir/taskReporter/ui/MainFrame.scala | 88 +++++++++++++++---- src/main/scala/preferences.scala | 69 +++++++++++++++ 8 files changed, 267 insertions(+), 26 deletions(-) create mode 100644 src/main/scala/jp/ymir/taskReporter/Preferences.scala create mode 100644 src/main/scala/jp/ymir/taskReporter/core/Report.scala create mode 100644 src/main/scala/jp/ymir/taskReporter/core/Task.scala create mode 100644 src/main/scala/preferences.scala diff --git a/build.sbt b/build.sbt index db126ad..ed20293 100644 --- a/build.sbt +++ b/build.sbt @@ -5,4 +5,7 @@ version := "0.0.1" libraryDependencies <+= scalaVersion { "org.scala-lang" % "scala-swing" % _ } +libraryDependencies += + "org.scala-tools.sbinary" %% "sbinary" % "0.4.2" + assemblySettings diff --git a/src/main/scala/jp/ymir/taskReporter/Main.scala b/src/main/scala/jp/ymir/taskReporter/Main.scala index 7f05460..94bd3c7 100644 --- a/src/main/scala/jp/ymir/taskReporter/Main.scala +++ b/src/main/scala/jp/ymir/taskReporter/Main.scala @@ -1,6 +1,7 @@ package jp.ymir.taskReporter import java.io._ import javax.swing._ +import jp.ymir.taskReporter._ import jp.ymir.taskReporter.ui._ object Main { @@ -11,8 +12,7 @@ object Main { def main(args: Array[String]) { try { - // FIXME: Provide a way to configure this. - UIManager.setLookAndFeel("javax.swing.plaf.metal.MetalLookAndFeel") + UIManager.setLookAndFeel(Preferences.lookAndFeel()) JFrame.setDefaultLookAndFeelDecorated(true) JDialog.setDefaultLookAndFeelDecorated(true) } diff --git a/src/main/scala/jp/ymir/taskReporter/Preferences.scala b/src/main/scala/jp/ymir/taskReporter/Preferences.scala new file mode 100644 index 0000000..d8b70ee --- /dev/null +++ b/src/main/scala/jp/ymir/taskReporter/Preferences.scala @@ -0,0 +1,31 @@ +package jp.ymir.taskReporter; +import java.awt.Dimension +import java.io._ +import sbinary._ +import sbinary.DefaultProtocol._ +import sbinary.Operations._ +import preferscala._ + +object AWTProtocol extends DefaultProtocol { + implicit object DimensionFormat extends Format[Dimension] { + def reads(in: Input) = read[(Int, Int)](in) match { + case (width, height) => + new Dimension(width, height) + } + def writes(out: Output, value: Dimension) = + write[(Int, Int)](out, (value.width, value.height)) + } +} + +object Preferences extends PackageGroup(PreferenceType.User) { + import AWTProtocol._ + + val lookAndFeel = + new Preference("ui/lookAndFeel", "javax.swing.plaf.metal.MetalLookAndFeel"); + + val mainFrameSize = + new Preference("ui/MainFrame/size", new Dimension(640, 480)); + + val lastChosenDir = + new Preference("ui/lastChosenDir", new File(System.getProperty("user.dir"))) +} diff --git a/src/main/scala/jp/ymir/taskReporter/core/Report.scala b/src/main/scala/jp/ymir/taskReporter/core/Report.scala new file mode 100644 index 0000000..5fa1ceb --- /dev/null +++ b/src/main/scala/jp/ymir/taskReporter/core/Report.scala @@ -0,0 +1,14 @@ +package jp.ymir.taskReporter.core +import java.util.Calendar +import scala.collection.immutable._ + +class Report(private val _date: Calendar) { + private var _tasks : Seq[Task] = Vector() + + def date = _date + + def +=(task: Task) { + require(task.date == _date) + _tasks = _tasks :+ task + } +} diff --git a/src/main/scala/jp/ymir/taskReporter/core/ReportSet.scala b/src/main/scala/jp/ymir/taskReporter/core/ReportSet.scala index f4392b7..0bbd672 100644 --- a/src/main/scala/jp/ymir/taskReporter/core/ReportSet.scala +++ b/src/main/scala/jp/ymir/taskReporter/core/ReportSet.scala @@ -1,14 +1,38 @@ package jp.ymir.taskReporter.core -import java.io._; +import java.io._ +import java.util.Calendar +import jp.ymir.taskReporter.core._ +import scala.collection.immutable._ +import scala.io._ class ReportSet(private var _file: Option[File]) { - private var _dirty : Boolean = false + private var _dirty = false + private var _reports : SortedMap[Calendar, Report] = TreeMap() - def file: Option[File] = { - return _file + if (!_file.isEmpty) { + val src = Source.fromFile(_file.get, "UTF-8") + for (line <- src.getLines) { + if (!line.isEmpty) { + val task = new Task(line) + if (_reports.isDefinedAt(task.date)) { + _reports(task.date) += task + } + else { + val report = new Report(task.date) + report += task + _reports = _reports + (report.date -> report) + } + } + } } - def dirty: Boolean = { - return _dirty + def file = _file + def file_=(f: Option[File]) { _file = f } + + def dirty = _dirty + + def save { + // FIXME + _dirty = false } } diff --git a/src/main/scala/jp/ymir/taskReporter/core/Task.scala b/src/main/scala/jp/ymir/taskReporter/core/Task.scala new file mode 100644 index 0000000..ebc8693 --- /dev/null +++ b/src/main/scala/jp/ymir/taskReporter/core/Task.scala @@ -0,0 +1,48 @@ +package jp.ymir.taskReporter.core +import java.util.Calendar +import java.util.GregorianCalendar +import scala.util.matching.Regex + +class Task(tsvLine: String) { + sealed abstract class Status + object Status { + case object NoProgress extends Status + case object DoingFine extends Status + case object Lagging extends Status + case object WillDelay extends Status + case object DeadlinePostponed extends Status + case object Completed extends Status + } + + class InvalidNumberOfColumnsException private(e: RuntimeException) extends RuntimeException(e) { + def this(msg: String) = this(new RuntimeException(msg)) + def this(msg: String, cause: Throwable) = this(new RuntimeException(msg, cause)) + } + + private val cols = tsvLine.split("\\t") + if (cols.length != 7) { + throw new InvalidNumberOfColumnsException(tsvLine) + } + + val date : Calendar = { + val pattern = """^(?:報告日:)?(\d{4})/(\d{2})/(\d{2})$""".r + cols(0) match { + case pattern(year, month, day) => + new GregorianCalendar(year.toInt, month.toInt, day.toInt) + } + } + + val ticketID : Int = { + val pattern = """^(?:チケットID:)?(\d+)$""".r + cols(1) match { + case pattern(id) => id.toInt + } + } + + val title : String = { + val pattern = """^(?:作業名:)?(.*)$""".r + cols(1) match { + case pattern(title) => title + } + } +} diff --git a/src/main/scala/jp/ymir/taskReporter/ui/MainFrame.scala b/src/main/scala/jp/ymir/taskReporter/ui/MainFrame.scala index 4b31c01..24b6004 100644 --- a/src/main/scala/jp/ymir/taskReporter/ui/MainFrame.scala +++ b/src/main/scala/jp/ymir/taskReporter/ui/MainFrame.scala @@ -1,24 +1,32 @@ package jp.ymir.taskReporter.ui import java.awt.Dimension +import java.awt.event.ComponentAdapter +import java.awt.event.ComponentEvent import java.io._ import javax.swing.JOptionPane import javax.swing.KeyStroke import javax.swing.event.MenuListener import javax.swing.event.MenuEvent +import javax.swing.filechooser.FileNameExtensionFilter import jp.ymir.taskReporter._ import jp.ymir.taskReporter.core._ import scala.swing._ import scala.swing.event._ class MainFrame(reportFile: Option[File]) extends Frame { - private val reportSet: ReportSet = new ReportSet(reportFile) + private var reportSet = new ReportSet(reportFile) - title = "Task Reporter" + Main.getVersion + title = "Task Reporter " + Main.getVersion - // FIXME: Provide a way to configure this. - size = new Dimension(640, 480) + size = Preferences.mainFrameSize() centerOnScreen + peer.addComponentListener(new ComponentAdapter() { + override def componentResized(e: ComponentEvent) { + Preferences.mainFrameSize() = size + } + }) + override def closeOperation { if (dirty) { val r = JOptionPane.showConfirmDialog( @@ -29,11 +37,8 @@ class MainFrame(reportFile: Option[File]) extends Frame { JOptionPane.YES_NO_CANCEL_OPTION); r match { - case JOptionPane.YES_OPTION => - // FIXME - dispose - case JOptionPane.NO_OPTION => - dispose + case JOptionPane.YES_OPTION => save; dispose + case JOptionPane.NO_OPTION => dispose case _ => } } @@ -46,28 +51,75 @@ class MainFrame(reportFile: Option[File]) extends Frame { return reportSet.dirty } + def save { + if (reportSet.file.isEmpty) { + val chooser = new FileChooser(Preferences.lastChosenDir()) + val r = chooser.showSaveDialog(null) + if (r != FileChooser.Result.Approve) { + return + } + + if (chooser.selectedFile.exists) { + val r = JOptionPane.showConfirmDialog( + peer, + "The chosen file or directory \"" + chooser.selectedFile.getName + "\" already exists.\n" + + "Do you want to overwrite it?", + "Confirmation", + JOptionPane.YES_NO_OPTION) + + r match { + case JOptionPane.YES_OPTION => + case JOptionPane.NO_OPTION => return + } + } + + Preferences.lastChosenDir() = chooser.selectedFile.getParentFile + reportSet.file = Some(chooser.selectedFile) + } + + reportSet.save + } + menuBar = new MenuBar { contents += new Menu("File") { mnemonic = Key.F - contents += new MenuItem(new Action("Open...") { + val miOpen = new MenuItem(new Action("Open...") { accelerator = Some(KeyStroke.getKeyStroke("control O")) def apply { - // FIXME + val chooser = new FileChooser(Preferences.lastChosenDir()) { + fileSelectionMode = FileChooser.SelectionMode.FilesOnly + fileFilter = new FileNameExtensionFilter("TSV files", "tsv") + title = "Select a report file to open..." + peer.setAcceptAllFileFilterUsed(false) + } + val r = chooser.showOpenDialog(null) + if (r == FileChooser.Result.Approve) { + Preferences.lastChosenDir() = chooser.selectedFile.getParentFile + reportSet = new ReportSet(Some(chooser.selectedFile)) + } } }) + contents += miOpen - val save = new MenuItem(new Action("Save") { + val miSave = new MenuItem(new Action("Save") { accelerator = Some(KeyStroke.getKeyStroke("control S")); - def apply { - // FIXME - } + def apply { save } }) - contents += save + contents += miSave + + contents += new Separator + + contents += new MenuItem(new Action("Quit") { + accelerator = Some(KeyStroke.getKeyStroke("control Q")) + def apply { closeOperation } + }) + peer.addMenuListener(new MenuListener { def menuSelected(e: MenuEvent) { - save.enabled = dirty - save.text = + miOpen.enabled = !dirty + miSave.enabled = dirty + miSave.text = if (reportSet.file.isEmpty) "Save..." else diff --git a/src/main/scala/preferences.scala b/src/main/scala/preferences.scala new file mode 100644 index 0000000..dc35ef8 --- /dev/null +++ b/src/main/scala/preferences.scala @@ -0,0 +1,69 @@ +// Borrowed from http://code.google.com/p/preferscala/ +package preferscala; +import sbinary._; +import sbinary.Operations._; +import java.io.File; +import java.util.prefs.Preferences; + +sealed abstract class PreferenceType{ + private[preferscala] def nodeForPackage(clazz : Class[_]) : Preferences; + private[preferscala] def root : Preferences; +} + +object PreferenceType{ + case object User extends PreferenceType{ + private[preferscala] def nodeForPackage(clazz : Class[_]) = Preferences.userNodeForPackage(clazz); + private[preferscala] def root = Preferences.userRoot; + } + + case object System extends PreferenceType{ + private[preferscala] def nodeForPackage(clazz : Class[_]) = Preferences.systemNodeForPackage(clazz); + private[preferscala] def root = Preferences.systemRoot; + } +} + +abstract class PreferenceGroup{ + def underlying : Preferences; + class Preference[T](name : String, default : => T)(implicit bin : Format[T]){ + private var localCache : T = null.asInstanceOf[T]; + + def apply() : T = { + if (localCache == null){ + var bytes = underlying.getByteArray(name, null); + if (bytes == null){ + localCache = default; + } else { + localCache = fromByteArray[T](bytes) + } + } + localCache + } + + def update(t : T){ + localCache = t; + underlying.putByteArray(name, toByteArray[T](t)); + } + + def reset(){ + localCache = null.asInstanceOf[T]; + underlying.remove(name); + underlying.flush(); + } + + def exportToFile(file : File){ + toFile(apply())(file); + } + + def loadFromFile(file : File){ + update(fromFile[T](file)); + } + } +} + +class NamedGroup(groupName : String, pt : PreferenceType) extends PreferenceGroup{ + val underlying = pt.root.node(groupName); +} + +abstract class PackageGroup(pt : PreferenceType) extends PreferenceGroup{ + val underlying = pt.nodeForPackage(getClass); +} -- 2.40.0