One To-Do list a day keeps the oblivion away
Organizing your life and work with a To-Do list, in Mathematica!
Because of the well-known reason, lots of people have temporarily changed to a quite different lifestyle. For some of us who stay at home and work from home for a long time, it can become hard to track a regular life and work routine. For many of us who are supposed to reduce our frequency of visiting crowded public place like a market, it would be a good practice to watch our pantries and have plans in advance for necessities.
For those kinds of purposes, To-Do list is a quite handy choice. To avoid installing yet another program for just a simple purpose, I decided to make my own using the flexible Notebook stack -- as I nearly always have a Mathematica running on desktop all the time. This small tool helped me a lot in last 2 months during which time I have to stay at home, so I'm sharing it to the community, hoping it can be of help for others.
What does it feel like? -- Experience of the To-Do list
My approach to a To-Do list is directly through notebook programming. But instead of heavily relying on Dynamic
framework, I try to minimize and localize it's usage as much as possible, so we can have a mostly static notebook.
The state pickers (the RadioButtonBars
at the right end) of item Cells are implemented in the stylesheet, so the To-Do list it self is pure static and easy to programly manipulate.
Currently the state of an item is encoded as style options of its cell, which is not good and subject to change. (I'm still looking for a nice and generic way to store data locally in a cell.)
Features
- Every item has a state picker at the right end:
The default stylesheet has 3 states:
- Multiple item levels are available, with progressive indentations:
- Items of different levels group and fold like Section/Subsection styles:
Shortcuts
- Pressing Alt + a key will create a new level 1 item cell.
- Pressing Enter key in an item cell will create a new item cell of the same level. This behaviour minics the Item/Subitem styles.
- Pressing Tab or * key at the beginning of an item cell will push it to 1 level down. Pressing Shift + Tab or Backspace key at the beginning of an item cell will pull it to 1 level up. This behaviour minics the Item/Subitem styles.
What does it feel like? -- Meta-experience of the To-Do list
Readers who are happy with our default To-Do stylesheet can safely ignore the section below, where we'll walk through how this To-Do list is made. But if you want to generate your own stylesheet, please do take a look.
As a highly programmable GUI, we are of course not stopping at one stylesheet. In fact we have this much more efficient way to generate To-Do list stylesheets as many as we want through notebook programming.
General design
Implementation of features
In general design, we indent cells with CellMargins
, stylize them with CellFrame
and CellFrameStyle
.
The "state picker" is implemented as a stand-alone cell in CellFrameLabels
. We'll talk about it later.
Section-like grouping behaviour is realized through setting proper CellGroupingRules
.
Implementation of shortcuts
MenuCommandKey
is used to implement 1st shortcut.
Two options, DefaultNewCellStyle
and ReturnCreatesNewCell
, are used to implement 2nd shortcut.
StyleKeyMapping
is used to implement 3rd shortcut.
The state picker
Our state picker is a RadioButtonBar[Dynamic[status, ...]]
, but we are doing more than usual inside the 2nd argument of Dynamic
.
Firstly we using notebook API SelectionMove
and SelectedCells
to locate whatever cell this controller lives in, then we change its style according to the localized controller state status
, a dummy variable that doesn't persist in the notebook file. (This behaviour is subject to change as soon as I determine how I want to persist data.)
RadioButtonBar[
Dynamic[status, (
status = #
; workCell = (
SelectionMove[EvaluationCell[], All, EvaluationCell]
; SelectedCells[][[1]]
)
; SetOptions[workCell
, FontVariations -> {"StrikeThrough" -> Not[status === defaultStatus]}
, FontColor -> colorOpt[status]
, Background -> If[status === defaultStatus, None, GrayLevel[0.95]]
]
) &]
, {val1 -> lbl1, val2 -> lbl2, ... }
]
Code
We put the full code we used to generate our To-Do list stylesheets here.
Helper functions
pipe = RightComposition;
branch = Through@*{##} &;
Configuration
A To-Do list stylesheet is fully described by two configuration variables.
State picker
myStatusConfig
is used to describe all possible states and their styles.
Any number of states should be fine, the first one will be used as the default state. Localization can be done by using different language in "txt"
.
myStatusConfig = <|
"value" -> {"todo", "done", "partial", "pass"}
, "txt" -> Map[Style[#, FontFamily -> "Segoe Script"] &, {"To-Do", "Done", "Half-Donw", "Pass"}]
, "color" -> { GrayLevel[0.2]
, RGBColor[0.5254901960784314, 0.7450980392156863, 0.19607843137254902`]
, RGBColor[1, 0.7450980392156863, 0.19607843137254902`], GrayLevel[0.75]
}|>;
To-Do list
myStyleConfig
is used to describe the appearance of the To-Do list itself.
"levelRange"
determines the most up and down levels the stylesheet will cover.
"indent0"
and "indentStep"
determine how the indentation behaves.
"frameRange"
determines range of thickness of the leading bar in front of each cell.
All ranges are running from the most up level to deepest level.
myStyleConfig = <|
"levelRange" -> {1, 6}
, "indent0" -> 10, "indentStep" -> 24
, "frameRange" -> {10, 2}
, "fontSizeRange" -> {13, 8}
, "colorFunc" -> ColorData["Rainbow"]
|>;
Construction of state picker
We have gone through the basic principle above. Here is the full code for it.
ClearAll[todoBar]
todoBar = With[{
defaultStatus = myStatusConfig[["value", 1]]
, colorOpt = myStatusConfig // pipe[
Lookup[{"value", "color"}]
, Apply@AssociationThread
]
, lblSet = myStatusConfig // pipe[
Lookup[{"value", "txt"}]
, MapThread[#1 -> Framed[#2, FrameMargins -> None, FrameStyle -> None] &]
]
},
DynamicModule[{status = defaultStatus, workCell}
,RadioButtonBar[Dynamic[status
, Function[
status = #
; workCell = (SelectionMove[EvaluationCell[], All, EvaluationCell]; SelectedCells[][[1]])
; SetOptions[workCell
, FontVariations -> {"StrikeThrough" -> Not[status === defaultStatus]}
, FontColor -> colorOpt[status]
, Background -> If[status === defaultStatus, None, GrayLevel[.95]]
]
]
]
, lblSet
, Method -> "Active", Appearance -> "Horizontal"]
] // Echo // ToBoxes //
Cell[BoxData[#]
, Background -> White
, FontWeight -> Bold, FontColor -> GrayLevel[0.2], FontSize -> 8
, FontVariations -> {"StrikeThrough" -> False}
, ShowStringCharacters -> False
] &
];
Construction of stylesheet
Generating an instance of the stylesheet from myStyleConfig
is basically applying a predefined template.
ClearAll[todoStylesheet]
todoStylesheet = With[{
levelRange = myStyleConfig@"levelRange"
, indent0 = myStyleConfig@"indent0",
indentStep = myStyleConfig@"indentStep"
, frameRange = myStyleConfig@"frameRange"
, fontSizeRange = myStyleConfig@"fontSizeRange"
, colorFunc = myStyleConfig@"colorFunc"
},
Module[{
lvlCounter = #,
currentLvl, superiorLvl, inferiorLvl,
frameThickness,
themeColor,
cellIndentMarg, groupingLvl,
ftsize, ftFamily
},
themeColor = lvlCounter //
pipe[
Rescale[#, levelRange, {0, 1}] &
, colorFunc, ColorConvert[#, "RGB"] &
]
;
ftsize = Rescale[lvlCounter, levelRange, fontSizeRange] // Round
;
ftFamily = Which[(*ftsize<=11*)lvlCounter > 1, "Arial", True, "Times"]
;
{currentLvl, cellIndentMarg, groupingLvl, frameThickness} =
{
StringJoin[{"TodoItem_", ToString@lvlCounter}]
, indent0 + indentStep (lvlCounter - 1)
, 15000 + 100 (lvlCounter - 1)
, Rescale[lvlCounter, levelRange, frameRange] // Ceiling
}
; superiorLvl = StringJoin[{"TodoItem_", ToString[lvlCounter - 1]}]
; inferiorLvl = StringJoin[{"TodoItem_", ToString[lvlCounter + 1]}]
;
Cell[
StyleData[currentLvl]
, CellFrame -> {{frameThickness, 1}, {1, 1}}
, System`CellFrameStyle -> {{themeColor, GrayLevel[1, 0]}, {GrayLevel[1, 0], GrayLevel[1, 0]}}
, CellFrameLabels -> {{None, todoBar}, {None, None}}
, CellFrameMargins -> {{10, 5}, {5, 1}}
, CellMargins -> {{cellIndentMarg, 50}, {1, 0}}
, System`ReturnCreatesNewCell -> True
, StyleKeyMapping -> {
"Tab" -> inferiorLvl
, "*" -> inferiorLvl
, "Backspace" -> superiorLvl
, System`KeyEvent["Tab", System`Modifiers -> {System`Shift}] -> superiorLvl
}
, CellGroupingRules -> {"SectionGrouping", groupingLvl}
, CellFrameLabelMargins -> 4
, DefaultNewCellStyle -> currentLvl
, CounterIncrements -> currentLvl
, CounterAssignments -> {{inferiorLvl, 0}}
, FontFamily -> ftFamily
, FontSize -> ftsize
, FontColor -> GrayLevel[0.2]
, If[lvlCounter == 1, Inactive[Rule][FontWeight, "Bold"], Inactive[Sequence][]]
, If[lvlCounter == 1, Inactive[Rule][MenuCommandKey, "a"], Inactive[Sequence][]]
]
] & /@ Range @@ levelRange //
Join[
{
Cell[StyleData[StyleDefinitions -> "Default.nb"]]
, Cell[StyleData["Notebook"]
, ShowCellBracket -> False
, ShowGroupOpener -> "Inline"
, System`TrackCellChangeTimes -> False
, Magnification -> 1]
}
, #
, {
Cell[StyleData["Text"]
, FontSize -> fontSizeRange[[2]] + 1
, CellMargins -> {{indent0 + indentStep (levelRange[[2]] - 1) + 10, 5}, {1, 0}}
]
}
] & // Activate
] //
Notebook[#, StyleDefinitions -> "Default.nb"] &;
We can have a look at the stylesheet:
NotebookPut@todoStylesheet
Using the stylesheet
If you are satisfied with your newly generated stylesheet, you can choose to save it to user's stylesheet folder:
FileNameJoin[{$UserBaseDirectory, "SystemFiles\\FrontEnd\\StyleSheets"}]
You might need to restart the Front End to actually see it under menu-bar » Format » Stylesheet.
Or you can use it directly as a private stylesheet:
Notebook[Join[
{Cell["Full Style List", "Section"]}
, pipe[StringTemplate["TodoItem_``"], Cell[#, #] &] /@ Range[6]
, {Cell["Text", "Text"]}
]
, StyleDefinitions -> todoStylesheet] //
NotebookPut
Hope this small To-Do list can be of help for those who need one. And it would be even better if you found notebook programming is interesting!
Attachments: