libraryDependencies <+=
scalaVersion { "org.scala-lang" % "scala-swing" % _ }
+libraryDependencies +=
+ "org.scala-tools.sbinary" %% "sbinary" % "0.4.2"
+
assemblySettings
package jp.ymir.taskReporter
import java.io._
import javax.swing._
+import jp.ymir.taskReporter._
import jp.ymir.taskReporter.ui._
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)
}
--- /dev/null
+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")))
+}
--- /dev/null
+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
+ }
+}
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
}
}
--- /dev/null
+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
+ }
+ }
+}
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(
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 _ =>
}
}
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
--- /dev/null
+// 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);
+}