Add a status bar.
authorGreg Burri <greg.burri@gmail.com>
Mon, 18 Jan 2016 16:33:57 +0000 (17:33 +0100)
committerGreg Burri <greg.burri@gmail.com>
Mon, 18 Jan 2016 16:33:57 +0000 (17:33 +0100)
Parasitemia/ParasitemiaCore/MainAnalysis.fs
Parasitemia/ParasitemiaUI/About.fs
Parasitemia/ParasitemiaUI/GUI.fs
Parasitemia/ParasitemiaUI/ParasitemiaUI.fsproj
Parasitemia/ParasitemiaUI/State.fs
Parasitemia/ParasitemiaUI/XAML/MainWindow.xaml

index 846c067..0faf54a 100644 (file)
@@ -23,7 +23,7 @@ let doAnalysis (img: Image<Bgr, byte>) (name: string) (config: Config) (reportPr
         | Some f -> f percent
         | _ -> ()
 
-    let inline buildLogWithName (text: string) = sprintf "(%s) %s" name text
+    let inline buildLogWithName (text:  string) = sprintf "(%s) %s" name text
     let logWithName mess = Log.User(buildLogWithName mess)
     let inline logTimeWithName (text: string) (f: unit -> 'a) : 'a = Log.LogWithTime((buildLogWithName text), Severity.USER, f)
 
index 5f9ce81..5f1be66 100644 (file)
@@ -20,7 +20,7 @@ let showWindow (parent: Window) =
     let txtAbout: TextBlock = ctrl "txtAbout"
 
     let version = System.Reflection.Assembly.GetEntryAssembly().GetName().Version
-    let txtVersion = sprintf "%d.%d.%d" version.Major version.Minor version.Revision
+    let txtVersion = sprintf " %d.%d.%d" version.Major version.Minor version.Revision
     txtAbout.Inlines.FirstInline.ElementEnd.InsertTextInRun(txtVersion)
 
 #if DEBUG
index 98b13ea..efe01c3 100644 (file)
@@ -33,6 +33,7 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
 
     let menuExit: MenuItem = ctrl "menuExit"
     let menuSaveFile: MenuItem = ctrl "menuSave"
+    let menuSaveAsFile: MenuItem = ctrl "menuSaveAs"
     let menuLoadFile: MenuItem = ctrl "menuOpen"
     let menuNewFile: MenuItem = ctrl "menuNew"
     let menuAddSourceImage: MenuItem = ctrl "menuAddSourceImage"
@@ -42,6 +43,9 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
     let menuHightlightRBC: MenuItem = ctrl "menuHightlightRBC"
     let menuAbout: MenuItem = ctrl "menuAbout"
 
+    let txtDocumentStatus: TextBlock = ctrl "txtDocumentStatus"
+    let txtMessageStatus: TextBlock = ctrl "txtMessageStatus"
+
     let txtPatient: TextBox = ctrl "txtPatient"
     let txtGlobalParasitemia: TextBox = ctrl "txtGlobalParasitemia"
 
@@ -50,7 +54,8 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
     let scrollViewCurrentImage: ScrollViewer = ctrl "scrollViewCurrentImage"
     let borderCurrentImage: Border = ctrl "borderCurrentImage"
     let canvasCurrentImage: Canvas = ctrl "canvasCurrentImage"
-    let txtImageInformation: TextBlock = ctrl "txtImageInformation"
+    let txtImageInformation1: TextBlock = ctrl "txtImageInformation1"
+    let txtImageInformation2: TextBlock = ctrl "txtImageInformation2"
 
     let scrollRBC: ScrollViewer = ctrl "scrollRBC"
     let stackRBC: StackPanel = ctrl "stackRBC"
@@ -87,6 +92,17 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
         frame.txtRBCNumber.Text <- rbc.num.ToString()
         frame
 
+    let updateDocumentStatus () =
+        txtDocumentStatus.Text <- if state.FilePath = "" then "<New document>" else state.FilePath
+
+    let statusMessageTimer = Threading.DispatcherTimer()
+    statusMessageTimer.Tick.AddHandler(fun obj args -> statusMessageTimer.Stop(); txtMessageStatus.Text <- "")
+    statusMessageTimer.Interval <- TimeSpan(0, 0, 2)
+    let displayStatusMessage (message: string) =
+        txtMessageStatus.Text <- message
+        statusMessageTimer.Stop()
+        statusMessageTimer.Start()
+
     let highlightRBCFrame (frame: Views.RBCFrame) (highlight: bool) =
         let rbc = frame.Tag :?> RBC
         if highlight
@@ -101,33 +117,38 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
         scrollViewCurrentImage.ScrollToHorizontalOffset(rbc.center.X * currentScale - scrollViewCurrentImage.ViewportWidth / 2. + borderCurrentImage.BorderThickness.Left)
         scrollViewCurrentImage.ScrollToVerticalOffset(rbc.center.Y * currentScale - scrollViewCurrentImage.ViewportHeight / 2. + borderCurrentImage.BorderThickness.Top)
 
-    let parasitemiaText (nbTotal: int, nbInfected: int) : string =
+    let percentText (nbTotal: int, nb: int) : string =
         if nbTotal = 0
         then
             ""
         else
-            let percent = 100. * (float nbInfected) / (float nbTotal)
-            sprintf "%.1f %% (%d / %d)" percent nbInfected nbTotal
+            let percent = 100. * (float nb) / (float nbTotal)
+            sprintf "%.1f %% (%d / %d)" percent nb nbTotal
 
     let updateCurrentImageInformation () =
         match state.CurrentImage with
         | Some srcImg ->
-            let parasitemiaStr = parasitemiaText (state.ImageParasitemia srcImg)
-            txtImageInformation.Inlines.Clear()
-            txtImageInformation.Inlines.Add(Documents.Run("Parasitemia: ", FontWeight = FontWeights.Bold))
-            txtImageInformation.Inlines.Add(parasitemiaStr)
-            txtImageInformation.Inlines.Add(Documents.LineBreak())
-
-            txtImageInformation.Inlines.Add(Documents.Run("Average erytrocyte diameter: ", FontWeight = FontWeights.Bold))
-            txtImageInformation.Inlines.Add(Documents.Run(srcImg.config.RBCRadius.ToString()))
-            txtImageInformation.Inlines.Add(Documents.LineBreak())
-
-            txtImageInformation.Inlines.Add(Documents.Run("Last analysis: ", FontWeight = FontWeights.Bold))
-            txtImageInformation.Inlines.Add(Documents.Run(if srcImg.dateLastAnalysis.Ticks = 0L then "<Never>" else srcImg.dateLastAnalysis.ToLocalTime().ToString()))
+            let parasitemiaStr = percentText (state.ImageParasitemia srcImg)
+            txtImageInformation1.Inlines.Clear()
+            txtImageInformation1.Inlines.Add(Documents.Run("Parasitemia: ", FontWeight = FontWeights.Bold))
+            txtImageInformation1.Inlines.Add(parasitemiaStr)
+            txtImageInformation1.Inlines.Add(Documents.LineBreak())
+
+            txtImageInformation1.Inlines.Add(Documents.Run("Last analysis: ", FontWeight = FontWeights.Bold))
+            txtImageInformation1.Inlines.Add(Documents.Run(if srcImg.dateLastAnalysis.Ticks = 0L then "<Never>" else srcImg.dateLastAnalysis.ToLocalTime().ToString()))
+
+            txtImageInformation2.Inlines.Clear()
+            let alteredStr = percentText (state.ImageNbAltered srcImg)
+            txtImageInformation2.Inlines.Add(Documents.Run("Number of erytrocytes manually altered: ", FontWeight = FontWeights.Bold))
+            txtImageInformation2.Inlines.Add(Documents.Run(alteredStr))
+            txtImageInformation2.Inlines.Add(Documents.LineBreak())
+
+            txtImageInformation2.Inlines.Add(Documents.Run("Average erytrocyte diameter: ", FontWeight = FontWeights.Bold))
+            txtImageInformation2.Inlines.Add(Documents.Run(srcImg.config.RBCRadius.ToString()))
         | _ -> ()
 
     let updateGlobalParasitemia () =
-        txtGlobalParasitemia.Text <- parasitemiaText state.GlobalParasitemia
+        txtGlobalParasitemia.Text <- percentText state.GlobalParasitemia
 
     let updateViewportPreview () =
         for preview in stackPreviews.Children |> Seq.cast<Views.ImageSourcePreview> do
@@ -237,23 +258,47 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
                 canvasCurrentImage.Children.[i].Visibility <- Visibility.Hidden
         | _ -> ()
 
+    let askDocumentPathToSave () : string option =
+        let dialog = SaveFileDialog(AddExtension = true, DefaultExt = PiaZ.extension, Filter = PiaZ.filter)
+
+        if state.FilePath <> ""
+        then
+            dialog.FileName <- FileInfo(state.FilePath).Name
+        elif state.PatientID <> ""
+        then
+            dialog.FileName <- state.PatientID + PiaZ.extension
+
+        let res = dialog.ShowDialog()
+        if res.HasValue && res.Value then
+            Some dialog.FileName
+        else
+            None
+
     let saveCurrentDocument () =
         try
             if state.FilePath = ""
             then
-                let dialog = SaveFileDialog(AddExtension = true, DefaultExt = PiaZ.extension, Filter = PiaZ.filter);
-                let res = dialog.ShowDialog()
-                if res.HasValue && res.Value
-                then
-                    state.FilePath <- dialog.FileName
+                match askDocumentPathToSave () with
+                | Some filepath ->
+                    state.FilePath <- filepath
                     state.Save()
+                | _ -> ()
             else
                 state.Save()
+            updateDocumentStatus ()
+            displayStatusMessage "Document saved"
         with
         | :? IOException as ex ->
             Log.Error(ex.ToString())
             MessageBox.Show(sprintf "The document cannot be save in '%s'" state.FilePath, "Error saving the document", MessageBoxButton.OK, MessageBoxImage.Error) |> ignore
 
+    let saveCurrentDocumentAsNewFile () =
+        match askDocumentPathToSave () with
+        | Some filepath ->
+            state.FilePath <- filepath
+            saveCurrentDocument ()
+        | _ -> ()
+
     // Ask the use to save the current document if neccessary.
     let askSaveCurrent () =
         if state.AlteredSinceLastSave
@@ -343,6 +388,7 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
         txtPatient.Text <- state.PatientID
         updatePreviews ()
         updateGlobalParasitemia ()
+        updateDocumentStatus ()
 
     let loadFile (filepath: string) =
         askSaveCurrent ()
@@ -355,15 +401,16 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
         | :? IOException as ex ->
             Log.Error(ex.ToString())
             state.FilePath <- previousFilePath
-            MessageBox.Show(sprintf "The document cannot be loaded from '%s'" state.FilePath, "Error saving the document", MessageBoxButton.OK, MessageBoxImage.Error) |> ignore
+            MessageBox.Show(sprintf "The document cannot be loaded from '%s'" state.FilePath, "Error loading the document", MessageBoxButton.OK, MessageBoxImage.Error) |> ignore
 
-    txtPatient.LostFocus.AddHandler(fun obj args -> state.PatientID <- txtPatient.Text)
+    txtPatient.TextChanged.AddHandler(fun obj args -> state.PatientID <- txtPatient.Text)
 
     menuExit.Click.AddHandler(fun obj args ->
         askSaveCurrent ()
         mainWindow.Root.Close())
 
     menuSaveFile.Click.AddHandler(fun obj args -> saveCurrentDocument ())
+    menuSaveAsFile.Click.AddHandler(fun obj args -> saveCurrentDocumentAsNewFile ())
 
     menuLoadFile.Click.AddHandler(fun obj args ->
         // TODO: if current state not saved and not empty, ask to save it.
@@ -385,8 +432,13 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
             let noSourceImage = state.SourceImages.Count() = 0
 
             for filename in dialog.FileNames do
-                let srcImg = state.AddSourceImage filename defaultConfig
-                addPreview srcImg
+                try
+                    let srcImg = state.AddSourceImage filename defaultConfig
+                    addPreview srcImg
+                with
+                | _ as ex ->
+                    Log.Error(ex.ToString())
+                    MessageBox.Show(sprintf "Unable to read the image from '%s'" filename, "Error adding an image", MessageBoxButton.OK, MessageBoxImage.Error) |> ignore
 
             updateGlobalParasitemia ()
 
@@ -491,9 +543,17 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
             borderCurrentImage.ReleaseMouseCapture()
             args.Handled <- true)
 
+    // Shortcuts.
+    mainWindow.Root.InputBindings.Add(
+        Input.KeyBinding(
+            FSharp.ViewModule.FunCommand((fun obj -> saveCurrentDocument ()), (fun obj -> true)),
+            Input.KeyGesture(Input.Key.S, Input.ModifierKeys.Control))) |> ignore
+
     // Viewport preview.
     scrollViewCurrentImage.ScrollChanged.AddHandler(fun obj args -> updateViewportPreview ())
 
+    updateDocumentStatus ()
+
     mainWindow.Root.Show()
 
     match fileToOpen with
index 3b88791..96643e9 100644 (file)
@@ -26,7 +26,7 @@
     <OutputPath>bin\Debug\</OutputPath>
     <DefineConstants>DEBUG;TRACE</DefineConstants>
     <WarningLevel>3</WarningLevel>
-    <PlatformTarget>x64</PlatformTarget>
+    <PlatformTarget>AnyCPU</PlatformTarget>
     <DocumentationFile>bin\Debug\ParasitemiaUI.XML</DocumentationFile>
     <Prefer32Bit>false</Prefer32Bit>
     <StartArguments>--folder "../../../Images/debug" --output "../../../Images/output" --debug</StartArguments>
@@ -38,7 +38,7 @@
     <Tailcalls>false</Tailcalls>
     <DefineConstants>DEBUG;TRACE</DefineConstants>
     <WarningLevel>3</WarningLevel>
-    <PlatformTarget>x64</PlatformTarget>
+    <PlatformTarget>AnyCPU</PlatformTarget>
     <DocumentationFile>bin\Debug\ParasitemiaUI.XML</DocumentationFile>
     <Prefer32Bit>false</Prefer32Bit>
     <StartArguments>--output "../../../Images/output" --debug</StartArguments>
index 5b89810..f133640 100644 (file)
@@ -30,6 +30,10 @@ type State () =
         List.length srcImg.rbcs,
         srcImg.rbcs |> List.fold (fun nbInfected rbc -> if rbc.infected then nbInfected + 1 else nbInfected) 0
 
+    member this.ImageNbAltered (srcImg: SourceImage) : int * int =
+        List.length srcImg.rbcs,
+        srcImg.rbcs |> List.fold (fun nbAltered rbc -> if rbc.setManually then nbAltered + 1 else nbAltered) 0
+
     member this.GlobalParasitemia : int * int =
         sourceImages
         |> Seq.fold (fun (nbTotal, nbTotalInfected) srcImg ->
@@ -56,16 +60,18 @@ type State () =
     /// <summary>
     /// Load the current state. 'FilePath' must have been defined.
     /// </summary>
-    /// <exception cref="System.IOException">If the file cannot be laoded</exception>
+    /// <exception cref="System.IOException">If the file cannot be loaded</exception>
     member this.Load () =
         let data = PiaZ.load this.FilePath
         this.PatientID <- data.patientID
         sourceImages.Clear()
         sourceImages.InsertRange(0, data.images)
-        if sourceImages.Count > 0
-        then this.CurrentImage <- Some sourceImages.[0]
+        this.CurrentImage <- if sourceImages.Count > 0 then Some sourceImages.[0] else None
         alteredSinceLastSave <- false
 
+    /// <summary>
+    /// </summary>
+    /// <exception cref="System.IOException">If the image cannot be read</exception>
     member this.AddSourceImage (filePath: string) (defaultConfig: ParasitemiaCore.Config.Config) : SourceImage =
         let srcImg = { num = sourceImages.Count + 1; config = defaultConfig.Copy(); dateLastAnalysis = DateTime(0L); rbcs = []; img = new Image<Bgr, byte>(filePath) }
         sourceImages.Add(srcImg)
index 37ea0c0..931b9b8 100644 (file)
@@ -3,13 +3,14 @@
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
         mc:Ignorable="d"
-        x:Name="MainWindow" Height="681.888" Width="787.61" MinHeight="200" MinWidth="300" Title="Parasitemia" Icon="pack://application:,,,/Resources/icon.ico">
+        x:Name="MainWindow" Height="700" Width="1000" MinHeight="200" MinWidth="300" Title="Parasitemia" Icon="pack://application:,,,/Resources/icon.ico" ResizeMode="CanResizeWithGrip">
    <DockPanel x:Name="dockPanelMain" LastChildFill="True">
       <Menu DockPanel.Dock="Top">
          <MenuItem Header="_File">
             <MenuItem x:Name="menuNew" Header="_New"  />
             <MenuItem x:Name="menuOpen" Header="_Open" />
             <MenuItem x:Name="menuSave" Header="_Save" />
+            <MenuItem x:Name="menuSaveAs" Header="_Save As..." />
             <Separator />
             <MenuItem x:Name="menuExit" Header="_Exit" />
          </MenuItem>
             <MenuItem x:Name="menuAbout" Header="_About" />
          </MenuItem>
       </Menu>
+      <StatusBar DockPanel.Dock="Bottom" >
+         <StatusBarItem>
+            <TextBlock Name="txtDocumentStatus" />
+         </StatusBarItem>
+         <StatusBarItem>
+            <TextBlock Name="txtMessageStatus" />
+         </StatusBarItem>
+      </StatusBar>
       <Grid x:Name="gridMain">
          <Grid.RowDefinitions>
             <RowDefinition Height="Auto"/>
             <ScrollViewer x:Name="scrollRBC" VerticalScrollBarVisibility="Hidden" HorizontalScrollBarVisibility="Visible" Grid.RowSpan="1"  Margin="3">
                <StackPanel x:Name="stackRBC" Orientation="Horizontal" />
             </ScrollViewer>
-            <TextBlock x:Name="txtImageInformation" Grid.Row="2" TextWrapping="Wrap" Margin="3" />
+            <Grid Grid.Row="2">
+               <Grid.ColumnDefinitions>
+                  <ColumnDefinition Width="1*"/>
+                  <ColumnDefinition Width="1*"/>
+               </Grid.ColumnDefinitions>
+               <TextBlock x:Name="txtImageInformation1" TextWrapping="Wrap" Margin="3" Grid.Column="0" />
+               <TextBlock x:Name="txtImageInformation2" TextWrapping="Wrap" Margin="3" Grid.Column="1" />
+            </Grid>
          </Grid>
       </Grid>
    </DockPanel>