14
$\begingroup$

I am making a GUI for one of my programs. The program is tasked with opening certain data files containing optical spectroscopy data and then doing things which are unrelated to this question.

The files are usually named something like "jul08a.big" which is not very descriptive, but will not be changed. To pick the right files I usually have to have my lab journal in front of me, check the measurement parameters against the filenames and select the right files. However, if I parse a few lines at the start and end of the file I can extract a lot of useful information, as per the image below (the underlying data structure is an association of associations and I'm not particularly struggling with this part).

enter image description here

The above grid will be eventually placed into something like CreateDialog which will have some kind of DialogReturn that will return a list of filenames of the files against which a checkmark has been placed.

What I need, however, is an efficient way to browse the directory tree on my machine. In other words, I would like to have some of the functionality of SystemDialogInput["FileOpen",...], but I would like to embed it into my Dialog. In other words, I'm looking to get the functionality of the left panel of the window below and replace the right panel with my own grid.

enter image description here

How can I have a simple directory tree browser / directory selector in a custom dialog of my own?

$\endgroup$
  • $\begingroup$ So for a selected directory you should see only filtered and pparsed files. Should it accumulate somewhere when you proceed to another directory? $\endgroup$ – Kuba Oct 19 '17 at 22:20
  • $\begingroup$ @Kuba I imagine that the directory tree panel will be something like a Dynamic which will run something like assoc = FileNames["newDirectory"<>"*.big"] // parsingFunction every time that the directory is changed. I don't need to store anything, since my parsingFunction seems to be reasonably fast. $\endgroup$ – LLlAMnYP Oct 19 '17 at 22:25
11
+150
$\begingroup$

Not perfect but a good start.

Openers expand/collapse trees but it does not affect the preview pane. Clicking on the directory name does.

enter image description here

foo[dir_] := {Button[FileNameTake[#], SystemOpen@#, 
      Appearance -> "FramedPalette"], FileDate[#], 
     FileByteCount[#]} & /@ 
   Select[FileNames["*", dir], Not@*DirectoryQ] // 
  Grid[Prepend[#, {dir, SpanFromLeft}], Alignment -> {Left, Center}, 
    Spacings -> {1, 2}] &

foo is up to you:

DynamicModule[{
  preview = "",
  init = $InstallationDirectory,
  directoryTree,
  directorySubTree
  },

 Dynamic@Panel[Pane[
    Grid[{{
       Pane[directoryTree@init, {200, Full}, Scrollbars -> True],
       Dynamic@preview}}, Alignment -> {Left, Top}]
    , {Full, 500}, Scrollbars -> False, AppearanceElements -> All, 
    BaseStyle -> LineBreakWithin -> False
    ], "Viewer"]
 ,
 UnsavedVariables :> {preview, directoryTree, directorySubTree},
 Initialization :> {
   preview = "";
   directoryTree[root_String?DirectoryQ] := 
    DynamicModule[{open = False, subTree},
     PaneSelector[
        {False -> #,
         True -> Column[{
            #, Dynamic[subTree]
            }]
         }, Dynamic@open, ImageSize -> Automatic
        ] &[
      Row[{Opener[
         Dynamic[open, {Automatic, 
           If[#, subTree = directorySubTree[root], subTree =.] &}]], 
        MouseAppearance[
         EventHandler[FileNameTake[root], 
          "MouseClicked" :> (preview = foo@root), Method -> "Queued"],
          "LinkHand"]}]
      ]
     ];

   directorySubTree[root_String?DirectoryQ] := Module[{
      subDirs, offset},
     subDirs = 
      Quiet@Check[Select[FileNames["*", root], DirectoryQ], $Failed];
     offset = Grid[{{Invisible@Opener[], #}}, Spacings -> {0, 0}] &;
     offset@Switch[subDirs,
       $Failed, 
       Row[{Style["\[WarningSign]", 20], " access denied"}, 
        BaseStyle -> Orange],
       {}, Style["\[FreakedSmiley]", Blue, 20],
       _, 
       Column[directoryTree /@ 
         Select[FileNames["*", root], DirectoryQ]]
       ]
     ]
   }

 ]
$\endgroup$
  • $\begingroup$ Looks interesting. I'll check this out in detail on Monday from work, but certainly +1 in advance $\endgroup$ – LLlAMnYP Oct 21 '17 at 23:02
  • $\begingroup$ Thanks a bunch! I've obtained nice results and summarized my solution below. BTW, you have a really nice stylesheet for .m files, would you care to share it? $\endgroup$ – LLlAMnYP Oct 30 '17 at 15:53
  • $\begingroup$ @LLlAMnYP Thanks, it is here: github.com/kubaPod/MoreStyles, and thanks for bounty, I'm glad I could help. $\endgroup$ – Kuba Oct 30 '17 at 19:55
6
$\begingroup$

Here's an extension on Kuba's interface:

Options[PaneFileBrowser] =
  {
   "FileFEEvents" -> {},
   "DirectoryFEEvents" -> {},
   "FileOnClick" -> Function[#],
   "FileOnDoubleClick" -> Function[SystemOpen@#],
   "DirectoryOnClick" -> Function[""],
   "DirectoryOnDoubleClick" ->
    Function[
     Set[Evaluate@Extract[#2["RootDirectory"], 1, HoldPattern], #];],
   "SelectFunction" ->
    Function[MatchQ[FileNameTake[#], Except[".DS_Store"]]],
   "SortFunction" -> Sort,
   "BottomBarElements" -> {}
   };
PaneFileBrowser[
  dir : (_String | _File)?DirectoryQ | Automatic : Automatic,
  disp :
   (
    Verbatim[Dynamic][dvar_, {__, f_}, ___?OptionQ] |
     Verbatim[Dynamic][dvar_, f_, ___?OptionQ] |
     Verbatim[Dynamic][dvar_, ___?OptionQ] | None
    ) : None,
  ops : OptionsPattern[]
  ] :=
 DynamicModule[{
   dirState = <||>,
   populateDirState,
   root = Replace[dir, Automatic :> Directory[]],
   browserElement,
   directoryTree,
   directorySubTree,
   eventHandlerElement,
   directoryOpener,
   sort = OptionValue["SortFunction"],
   selFunc = OptionValue["SelectFunction"],
   filOnClick = OptionValue["FileOnClick"],
   dirOnClick = OptionValue["DirectoryOnClick"],
   filOnDubClick = OptionValue["FileOnDoubleClick"],
   dirOnDubClick = OptionValue["DirectoryOnDoubleClick"],
   extraFileEvents = OptionValue["FileFEEvents"],
   extraDirEvents = OptionValue["DirectoryFEEvents"],
   pvar,
   prevVar,
   prevFunc = If[Length@{f} == 0, Set, f],
   dispSize = 650*{1, 1/GoldenRatio},
   barWidth = 200,
   topHeight = 50,
   bottomHeight = 25,
   bbels
   },
  dispSize = dispSize - {0, topHeight + bottomHeight};
  prevVar =
   If[disp =!= None,
    dvar = "";
    Dynamic[dvar],
    pvar = "";
    Dynamic[pvar]
    ];
  extraDirEvents =
   OptionValue["DirectoryFEEvents"] /. {
     #["RootDirectory"] :> Dynamic[root],
     #["DirectoryTree"] :> dirState,
     #["DisplayVariable"] :> prevVar,
     #["PaneSize"] :> Dynamic[dispSize - {barWidth, 0}]
     };
  extraFileEvents =
   OptionValue["FileFEEvents"] /. {
     #["RootDirectory"] :> Dynamic[root],
     #["DirectoryTree"] :> dirState,
     #["DisplayVariable"] :> prevVar,
     #["PaneSize"] :> Dynamic[dispSize - {barWidth, 0}]
     };
  bbels =
   Replace[OptionValue["BottomBarElements"],
     Except[_List] -> {}
     ] /.
    {
     #["RootDirectory"] :> Dynamic[root],
     #["DirectoryTree"] :> Dynamic@dirState,
     #["DisplayVariable"] :> prevVar,
     #["PaneSize"] :> Dynamic[dispSize - {barWidth, 0}]
     };
  Framed[#, FrameStyle -> GrayLevel[.8], 
     FrameMargins -> {{0, 0}, {0, 0}}] &@
   Column[{
     Panel[
      Row@{
        Spacer[10],
        Dynamic[
         InputField[Dynamic[root, If[DirectoryQ[#], Set[root, #]] &], 
          String,
          FieldSize -> {{50, 100}, 1}
          ],
         Function[
          root = #;
          dirState = <||>;
          populateDirState[#]
          ]
         ]
        },
      ImageSize -> {Dynamic[dispSize[[1]]], topHeight},
      Alignment -> {Left, Center},
      Appearance ->
       {
        "Default" ->
         Lookup[

          FrontEndResource["FEExpressions", 
           "MoreLeftSetterNinePatchAppearance"],
          "Hover"
          ]
        }
      ],
     Pane[
      Grid[{
        {
         Pane[
          Dynamic@directoryTree@root,
          Dynamic[
           {
            barWidth,
            dispSize[[2]]
            },
           Set[barWidth, #] &
           ],
          Scrollbars -> True
          ],
         Pane[
          Dynamic@prevVar,
          Dynamic[dispSize - {barWidth, 0}]
          ]
         }
        },
       Alignment -> {Left, Top},
       Spacings -> {0, 0}
       ],
      Dynamic[dispSize],
      BaseStyle -> LineBreakWithin -> False,
      AppearanceElements -> {"ResizeArea"},
      Scrollbars -> False
      ],
     Item[#,
        Alignment -> {Center, Bottom},
        FrameStyle -> GrayLevel[.8],
        Frame -> {None, None, GrayLevel[.8], None}
        ] &@
      Panel[
       Style[
        Grid[{bbels},
         Alignment -> {Left, Bottom},
         RowMinHeight -> 0,
         Spacings -> {0, 0}
         ],
        ButtonBoxOptions ->
         {
          ButtonMinHeight -> 0
          }
        ],
       ImageMargins -> {{0, 0}, {-2, 1}},
       ImageSize -> {Dynamic[dispSize[[1]]], {bottomHeight, 1000}},
       Appearance ->
        {
         "Default" ->
          Lookup[

           FrontEndResource["FEExpressions", 
            "MoreLeftSetterNinePatchAppearance"],
           "Default"
           ]
         },
       FrameMargins -> None,
       Alignment -> {Left, Center},
       ContentPadding -> False
       ]
     },
    Spacings -> 0,
    RowMinHeight -> 0
    ],
  UnsavedVariables :> {dirState, directoryTree, directorySubTree},
  Initialization :> {
    dirState = <||>,
    populateDirState[d_] :=
     dirState[d] =
      <|
       "Open" -> False,
       "Children" ->
        Replace[Quiet@Check[FileNames["*", d], $Failed],
         l_List :> sort@Select[l, selFunc]
         ]
       |>,
    populateDirState[root],
    directoryTree[d_String?DirectoryQ] :=
     PaneSelector[{
         False -> #,
         True :>
          Dynamic@Column[{#, directorySubTree[d]}]
         },
        Dynamic@TrueQ@dirState[d, "Open"],
        ImageSize -> Automatic
        ] &[
      Row[{
        directoryOpener[d],
        eventHandlerElement[d]
        }]
      ],
    directorySubTree[d_String?DirectoryQ] :=
     Grid[{{
        Invisible[Opener[]],
        Column[
         browserElement /@ dirState[d, "Children"]
         ]
        }},
      Spacings -> {0, 0}
      ],
    directoryOpener[d_] :=
     Opener[
      Dynamic[TrueQ@dirState[d, "Open"],
       {
        Automatic,
        Function[
         dirState[d, "Open"] = #;
         If[#,
          populateDirState /@
           dirState[d, "Children"],
          Map[
           Function[dirState[#] =.],
           dirState[d, "Children"]
           ]
          ]
         ]
        }
       ],
      Enabled -> Length@dirState[d, "Children"] > 0
      ],
    eventHandlerElement[d_String?FileExistsQ] :=

     MouseAppearance[
      EventHandler[
       If[dirState[d, "Children"] === $Failed,
        Tooltip[
         Style[Row@{"\[WarningSign] ", FileNameTake[d]}, Orange],
         "Access Denied"
         ],
        FileNameTake[d]
        ],
       {
        With[{ev = Extract[prevVar, 1, Unevaluated]},
         "MouseClicked" :>
          prevFunc[ev,
           Replace[Null -> ""]@
            If[CurrentValue["MouseClickCount"] == 2,
              If[DirectoryQ@d,
               dirOnDubClick,
               filOnDubClick
               ],
              If[DirectoryQ@d,
               dirOnClick,
               filOnClick
               ]
              ][d,
             <|
              "RootDirectory" :> Dynamic@root,
              "DirectoryTree" :> Dynamic@dirState,
              "DisplayVariable" :> prevVar,
              "PaneSize" :> Dynamic[dispSize]
              |>
             ]
           ]
         ],
        If[DirectoryQ@d,
         Sequence @@ extraDirEvents,
         Sequence @@ extraFileEvents
         ],
        Method -> "Queued"
        }
       ],
      "LinkHand"
      ],
    browserElement[d_String?DirectoryQ] :=
     directoryTree[d],
    browserElement[d_String?FileExistsQ] :=
     eventHandlerElement[d]
    }
  ]

All I did was polish some stuff up and extend it to also work with files.

It can be used raw or with a specified directory and can take a standard Dynamic object as the second argument to be used as the display variable.

Here's how you might use it to do something like what you want:

PaneFileBrowser[
 FileNameJoin@{$RootDirectory, "Library"},
 Dynamic[display],
 "SelectFunction" ->
  DirectoryQ,
 "DirectoryOnClick" ->
  Function[
   If[Length@Select[FileNames["*", #], Not@*DirectoryQ] > 0,
    ListPicker[
     Dynamic[selectedFiles],
     Select[FileNames["*", #], Not@*DirectoryQ],
     ImageSize -> #2["PaneSize"],
     Background -> {{GrayLevel[.95], White}},
     Appearance -> "Frameless"
     ],
    ""
    ]
   ],
 "BottomBarElements" ->
  {
   Spacer[10],
   Framed[
    Button[Style["View File", GrayLevel[.3]],
     display = Panel@Import[selectedFiles[[1]], "Text"],
     Appearance ->
      FrontEndResource["FEExpressions", 
       "MoreLeftSetterNinePatchAppearance"],
     ImageSize -> {Automatic, 25},
     FrameMargins -> 5
     ],
    FrameMargins -> None,
    FrameStyle -> GrayLevel[.8]
    ]
   }
 ]

This only displays directories on the left. When a directory is clicked, it opens a ListPicker on the right of files. That binds to a selection variable, and the button at the bottom of the interface uses that selection variable to open the first selected file in a Panel on the right. It looks like this:

doop

Here's what it looks like after clicking "View File":

doop2

I've put it in a package here

So you can just run it like:

Get["https://raw.githubusercontent.com/b3m2a1/mathematica-tools/master/PaneFileBrowser.wl"];
PaneFileBrowser[]
$\endgroup$
  • $\begingroup$ With you around one really has to emphasize that udates are incomming, now I have to go further with them before posting :P $\endgroup$ – Kuba Oct 29 '17 at 7:12
  • $\begingroup$ @Kuba it just seemed like a fun little thing to extend :) $\endgroup$ – b3m2a1 Oct 29 '17 at 7:23
  • $\begingroup$ I cleaned part of comments related to past issues. $\endgroup$ – Kuba Oct 29 '17 at 10:13
  • $\begingroup$ Wow, this looks really nice. However, your second code-block doesn't do anything for me, i.e. only the last code-block with an import from git works and the right pane has no functionality. Is your code dependent on v11+? I'm running 10.2 on a Windows machine. In any case +1 and if this starts working, it's going to be a tough decision between yours and Kuba's answers. $\endgroup$ – LLlAMnYP Oct 30 '17 at 10:11
  • $\begingroup$ Got it to work, {$RootDirectory, "Library"} seems to be mac-specific. I'm sorry I don't have time to share my efforts and give feedback in great detail (implementing this very app at work is consuming all the time I have), but I learned a bunch from the two answers here and they've enabled me to build upon your prototypes for my own needs. $\endgroup$ – LLlAMnYP Oct 30 '17 at 10:22
3
$\begingroup$

I'd like to summarize my efforts wherein I adapted Kuba's code for my needs.

Working with files is often platform-specific, so the code below assumes Windows.

The full program for which I needed this is available in my Git account. A minimal example is also presented here.

The folder structure as follows is necessary:

(* root folder of app *)
  dev
    Directory_browser.m (* this is just the MWE, it's not needed per se *)
  sampleBIGfolder
    nov30a.big
    nov30b.big  (* these are the sample data files *)
    ...
  scanDir.exe (* external program written in C to parse folder contents *)
  cache
    avcnup105971 (* this folder stores a cache of parsed directories *)
    ...

This is partially mimicked by my repository.

Assuming this folder structure, we can now inspect the code of Directory_browser.m

SetDirectory[NotebookDirectory[]];
SetDirectory[".."]; (* go to root folder of app *)

load[dir_] := (
    Run["scanDir.exe","\""<>#<>"\"",IntegerString[Hash[#],36],IntegerString[Hash[FileNames["*.big",#]],36]]&[dir];
    With[{strm = OpenRead[FileNameJoin[{Directory[],"cache", IntegerString[Hash[dir],36]}]]},
        ReadList[strm, String, 1];
        Block[{out = ReadList[strm,{Word,Word,Number,Number,Number,Number,Number,Number,Number,Word},WordSeparators->{"\t"}]},
            Close[strm];
            out
        ]
    ])

load runs my external C-program to parse the contents of all .big files in a given directory. It stores the results in a file ./cache/*hash* where *hash* is the result of IntegerString[Hash[dir],36] on the directory name. The first line of this file is the result of IntegerString[Hash[FileNames["*.big",dir]],36]]. If the file exists and the hashes match, there is no need to parse the directory again and the external program terminates. load then proceeds to read the remainder of that file (which either exists and was possibly rewritten or was just generated by scanDir.exe). The remainder of the file contains a bunch of tab-separated values.

SetAttributes[render, HoldAll];
render[preview_, selected_] :=
    Join[{MapThread[Function[{name,num},Button[name,preview=SortBy[preview,#[[num]]&]]],
            {{"Name","Comment","T","AOI","POLA","Scans","Resolution","Min","Max","Range"},Range[10]}]},
        Map[{Button[FileNameTake[#[[1]]],AppendTo[selected,#[[1]]]]}~Join~#[[2;;]]&, preview]] //
    Grid[#, Background -> {{GrayLevel[1], None, GrayLevel[1]},{{GrayLevel[0.85], RGBColor[0.88, 1, 0.88]}}}, Frame -> All]&

render generates a nice-looking grid like so:

grid

The buttons at the top of each column sort the grid by the respective column values. The buttons at the left append the fully-qualified filename to another list. The remaining columns are measurement parameters parsed from the files - something I wanted to see on my screen instead of digging through the lab journal.

SetAttributes[remover,HoldFirst];
remover[list_] := Column@MapIndexed[Button[#,list=Delete[list,First@#2]]&,list]

This snippet of code shows each element of the list with the full filenames as buttons - clicking on them removes those elements from the list.

preload[path_]:=With[{dirs=FileNameJoin[FileNameSplit[path][[;;#]]]&/@Range[Length@FileNameSplit[path]]},
DynamicModule[
    {preview={},selected={},init=Select[#<>":\\"&/@CharacterRange["A","Z"],DirectoryQ],directoryTree,directorySubTree},
    Dynamic[
        Panel[
            Pane[
                Grid[{{
                    Pane[Column[directoryTree/@init],{350,Full},Scrollbars->True],
                    Pane[Dynamic[render[preview,selected]],{Full,800},Scrollbars->True],
                    Dynamic[remover@selected]}}, Alignment->{Left,Top}],
                {Full,800},Scrollbars->{False,True},AppearanceElements->All,BaseStyle->LineBreakWithin->False
            ],
            "Viewer"
        ],
        UnsavedVariables :> {preview,directoryTree,directorySubTree},
        Initialization :> {
            selected={};
            preview={};
            directoryTree[root_String?DirectoryQ] :=
                DynamicModule[{open=MemberQ[dirs,root],subTree=If[MemberQ[dirs,root],directorySubTree[root],""]},
                    PaneSelector[{
                        False -> #,
                        True -> Column[{#,
                            Dynamic[subTree]}]}, Dynamic[open],ImageSize->Automatic] & [
                        Row[{
                            Opener[Dynamic[open,{Automatic,If[#,subTree=directorySubTree[root],subTree=.]&}]],
                            MouseAppearance[EventHandler[
                                FileNameTake[root],
                                "MouseClicked":>(preview=load[root]),Method->"Queued"],"LinkHand"]
                        }]
                    ]
                ];
            directorySubTree[root_String?DirectoryQ] :=
                Module[{subDirs,offset},
                    subDirs = Quiet@Check[Select[FileNames["*",root],DirectoryQ],$Failed];
					offset=Grid[{{Invisible@Opener[],#}},Spacings->{0,0}]&;
					offset@Switch[subDirs,
						$Failed,Row[{Style["\[WarningSign]",20]," access denied"},BaseStyle->Orange],
                        {},Style["\[FreakedSmiley]",Blue,20],
                        _,Column[directoryTree/@Select[FileNames["*",root],DirectoryQ]]]
                ]
        }
    ]
] ]

Finally, this is Kuba's code with some modifications. I made it initialize with all valid drives available (something like the default Windows shortcuts, e.g., Favorites, etc., would have been nice, but not sure how to do that); also the tree view of the folders is expanded from the root of its drive through to the first argument of preload. The middle pane contains the list of .big files in the selected folder rendered by render, the rightmost pane has the list of selected files.

CreateDialog[preload[Directory[]]]

then browsing to the sampleBIGfolder and clicking on it will show this:

dialog

The C code is of course available over at github, as are the sample data files. Since I based this on Kuba's code, it's only fair that the bounty goes to him. Thanks!

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.