]> gitweb @ CieloNegro.org - task-reporter.git/commitdiff
wip
authorPHO <pho@cielonegro.org>
Mon, 29 Sep 2014 17:29:34 +0000 (02:29 +0900)
committerPHO <pho@cielonegro.org>
Mon, 29 Sep 2014 17:29:34 +0000 (02:29 +0900)
build.sbt
src/main/scala/jp/ymir/taskReporter/Main.scala
src/main/scala/jp/ymir/taskReporter/Preferences.scala [new file with mode: 0644]
src/main/scala/jp/ymir/taskReporter/core/Report.scala [new file with mode: 0644]
src/main/scala/jp/ymir/taskReporter/core/ReportSet.scala
src/main/scala/jp/ymir/taskReporter/core/Task.scala [new file with mode: 0644]
src/main/scala/jp/ymir/taskReporter/ui/MainFrame.scala
src/main/scala/preferences.scala [new file with mode: 0644]

index db126ad662b012bf56b8e7357d2d23c845b2a2bf..ed20293d1985a6b930fe44de14da44802fe70194 100644 (file)
--- 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
index 7f054603ee8dfe893e90bfc297ad921ecbe2642d..94bd3c78374be41a1a5353883fe6d46efc8d4099 100644 (file)
@@ -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 (file)
index 0000000..d8b70ee
--- /dev/null
@@ -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 (file)
index 0000000..5fa1ceb
--- /dev/null
@@ -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
+  }
+}
index f4392b76e41d7c3993d0e91ef664b1b073fbe4b9..0bbd67276261c34bf6d645337ef1708c2ecc5987 100644 (file)
@@ -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 (file)
index 0000000..ebc8693
--- /dev/null
@@ -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
+    }
+  }
+}
index 4b31c01270822fe6476d1756514a2a991e8245a2..24b60046b12b120358104e5dfb13d985e54041a6 100644 (file)
@@ -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 (file)
index 0000000..dc35ef8
--- /dev/null
@@ -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);
+}