Compare commits
2 Commits
v1.0.0-SNA
...
main
Author | SHA1 | Date |
---|---|---|
|
c25f04ad15 | |
|
aded0b6400 |
3
dub.json
3
dub.json
|
@ -4,7 +4,8 @@
|
||||||
],
|
],
|
||||||
"copyright": "Copyright © 2022, Andrew Lalis",
|
"copyright": "Copyright © 2022, Andrew Lalis",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"gtk-d": "~>3.10.0"
|
"gtk-d": "~>3.10.0",
|
||||||
|
"dsh": "~>1.6.1"
|
||||||
},
|
},
|
||||||
"stringImportPaths": [
|
"stringImportPaths": [
|
||||||
"resources"
|
"resources"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"fileVersion": 1,
|
"fileVersion": 1,
|
||||||
"versions": {
|
"versions": {
|
||||||
|
"dsh": "1.6.1",
|
||||||
"gtk-d": "3.10.0"
|
"gtk-d": "3.10.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,6 +107,7 @@
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="use_underline">True</property>
|
<property name="use_underline">True</property>
|
||||||
<property name="use_stock">True</property>
|
<property name="use_stock">True</property>
|
||||||
|
<signal name="activate" handler="onAboutMenuActivated" swapped="no"/>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
|
@ -133,7 +134,7 @@
|
||||||
<object class="GtkListBox" id="taskList">
|
<object class="GtkListBox" id="taskList">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="selection_mode">none</property>
|
<property name="activate_on_single_click">False</property>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
|
@ -153,6 +154,7 @@
|
||||||
<object class="GtkEntry" id="addTaskEntry">
|
<object class="GtkEntry" id="addTaskEntry">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
|
<signal name="activate" handler="onAddTask" swapped="no"/>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">True</property>
|
<property name="expand">True</property>
|
||||||
|
@ -169,7 +171,6 @@
|
||||||
<property name="use_stock">True</property>
|
<property name="use_stock">True</property>
|
||||||
<property name="always_show_image">True</property>
|
<property name="always_show_image">True</property>
|
||||||
<signal name="clicked" handler="onAddTask" swapped="no"/>
|
<signal name="clicked" handler="onAddTask" swapped="no"/>
|
||||||
<accelerator key="Return" signal="clicked"/>
|
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">False</property>
|
<property name="expand">False</property>
|
||||||
|
|
130
source/app.d
130
source/app.d
|
@ -1,5 +1,10 @@
|
||||||
import model;
|
import model;
|
||||||
|
|
||||||
import std.stdio;
|
import std.stdio;
|
||||||
|
import std.functional : toDelegate;
|
||||||
|
import dsh : getHomeDir;
|
||||||
|
import std.path;
|
||||||
|
import std.file;
|
||||||
|
|
||||||
import gtk.MainWindow;
|
import gtk.MainWindow;
|
||||||
import gtk.Main;
|
import gtk.Main;
|
||||||
|
@ -14,6 +19,10 @@ import gtk.Label;
|
||||||
import gtk.CheckButton;
|
import gtk.CheckButton;
|
||||||
import gtk.ToggleButton;
|
import gtk.ToggleButton;
|
||||||
import gtk.Button;
|
import gtk.Button;
|
||||||
|
import gtk.Widget;
|
||||||
|
|
||||||
|
import gdk.Keymap;
|
||||||
|
import gdk.Keysyms : GdkKeysyms;
|
||||||
import gio.Resource;
|
import gio.Resource;
|
||||||
import glib.Bytes;
|
import glib.Bytes;
|
||||||
|
|
||||||
|
@ -30,26 +39,6 @@ class ToDoItemWidget : Box {
|
||||||
label.setHalign(GtkAlign.START);
|
label.setHalign(GtkAlign.START);
|
||||||
label.setValign(GtkAlign.CENTER);
|
label.setValign(GtkAlign.CENTER);
|
||||||
this.packStart(button, false, false, 0);
|
this.packStart(button, false, false, 0);
|
||||||
|
|
||||||
Button removeButton = new Button(StockID.REMOVE);
|
|
||||||
removeButton.addOnClicked(delegate(Button b) {
|
|
||||||
todoModel.removeItem(item.priority);
|
|
||||||
});
|
|
||||||
this.packEnd(removeButton, false, false, 0);
|
|
||||||
|
|
||||||
if (todoModel.canIncrement(item)) {
|
|
||||||
Button upButton = new Button(StockID.GO_UP, delegate(Button b) {
|
|
||||||
todoModel.incrementPriority(item);
|
|
||||||
});
|
|
||||||
this.packEnd(upButton, false, false, 0);
|
|
||||||
}
|
|
||||||
if (todoModel.canDecrement(item)) {
|
|
||||||
Button downButton = new Button(StockID.GO_DOWN, delegate(Button b) {
|
|
||||||
todoModel.decrementPriority(item);
|
|
||||||
});
|
|
||||||
this.packEnd(downButton, false, false, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.packEnd(label, true, true, 0);
|
this.packEnd(label, true, true, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,31 +66,110 @@ void main(string[] args) {
|
||||||
builder.addCallbackSymbol("onSaveAsMenuActivated", &onSaveAsMenuActivated);
|
builder.addCallbackSymbol("onSaveAsMenuActivated", &onSaveAsMenuActivated);
|
||||||
builder.addCallbackSymbol("onOpenMenuActivated", &onOpenMenuActivated);
|
builder.addCallbackSymbol("onOpenMenuActivated", &onOpenMenuActivated);
|
||||||
builder.addCallbackSymbol("onQuitMenuActivated", &onWindowDestroy);
|
builder.addCallbackSymbol("onQuitMenuActivated", &onWindowDestroy);
|
||||||
|
builder.addCallbackSymbol("onAboutMenuActivated", &onAboutMenuActivated);
|
||||||
builder.connectSignals(null);
|
builder.connectSignals(null);
|
||||||
|
|
||||||
taskList = cast(ListBox) builder.getObject("taskList");
|
taskList = cast(ListBox) builder.getObject("taskList");
|
||||||
|
Widget listWidget = cast(Widget) taskList;
|
||||||
|
listWidget.addOnKeyPress(toDelegate(&taskListKeyPressed));
|
||||||
taskEntry = cast(Entry) builder.getObject("addTaskEntry");
|
taskEntry = cast(Entry) builder.getObject("addTaskEntry");
|
||||||
|
|
||||||
todoModel = new ToDoModel();
|
todoModel = new ToDoModel();
|
||||||
import std.functional : toDelegate;
|
string lastOpenPath = buildPath(getHomeDir(), ".config/todo-d/last-open.txt");
|
||||||
todoModel.addListener(ModelUpdateListener.of(toDelegate(&itemsUpdated)));
|
if (exists(lastOpenPath)) {
|
||||||
|
import std.string : strip;
|
||||||
|
string lastOpenFile = readText(lastOpenPath).strip;
|
||||||
|
todoModel.openFromJson(lastOpenFile);
|
||||||
|
}
|
||||||
|
|
||||||
window = cast(ApplicationWindow) builder.getObject("window");
|
window = cast(ApplicationWindow) builder.getObject("window");
|
||||||
|
auto listener = new UIModelUpdateListener(window);
|
||||||
|
todoModel.addListener(listener);
|
||||||
|
|
||||||
|
// Trigger UI updates once before rendering the window.
|
||||||
|
todoModel.notifyListeners();
|
||||||
|
listener.fileUpdated(todoModel.getOpenFilename());
|
||||||
|
|
||||||
window.showAll();
|
window.showAll();
|
||||||
Main.run();
|
Main.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class UIModelUpdateListener : ModelUpdateListener {
|
||||||
|
private ApplicationWindow window;
|
||||||
|
|
||||||
|
this(ApplicationWindow window) {
|
||||||
|
this.window = window;
|
||||||
|
}
|
||||||
|
|
||||||
void itemsUpdated(ToDoItem[] items) {
|
void itemsUpdated(ToDoItem[] items) {
|
||||||
taskList.removeAll();
|
taskList.removeAll();
|
||||||
foreach (item; items) {
|
foreach (item; items) {
|
||||||
auto widget = new ToDoItemWidget(item, todoModel);
|
auto widget = new ToDoItemWidget(item, todoModel);
|
||||||
auto row = new ListBoxRow();
|
auto row = new ListBoxRow();
|
||||||
|
row.setSelectable(true);
|
||||||
|
row.setActivatable(false);
|
||||||
row.add(widget);
|
row.add(widget);
|
||||||
taskList.add(row);
|
taskList.add(row);
|
||||||
}
|
}
|
||||||
taskList.showAll();
|
taskList.showAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void fileUpdated(string filename) {
|
||||||
|
if (filename is null) {
|
||||||
|
window.setTitle("todo-d");
|
||||||
|
} else {
|
||||||
|
window.setTitle("todo-d - " ~ filename);
|
||||||
|
}
|
||||||
|
window.showAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool taskListKeyPressed(GdkEventKey* event, Widget w) {
|
||||||
|
int idx = taskList.getSelectedRow().getIndex();
|
||||||
|
int selectedPrio = idx + 1;
|
||||||
|
ToDoItem selectedItem = todoModel.getItemAt(selectedPrio);
|
||||||
|
if (selectedItem is null) return true;
|
||||||
|
bool ctrlDown = (event.state & ModifierType.CONTROL_MASK) > 0;
|
||||||
|
if (ctrlDown && event.keyval == GdkKeysyms.GDK_Up && todoModel.canIncrement(selectedItem)) {
|
||||||
|
todoModel.incrementPriority(selectedItem);
|
||||||
|
auto row = taskList.getRowAtIndex(idx - 1);
|
||||||
|
taskList.selectRow(row);
|
||||||
|
Widget rowWidget = cast(Widget) row;
|
||||||
|
rowWidget.grabFocus();
|
||||||
|
}
|
||||||
|
if (ctrlDown && event.keyval == GdkKeysyms.GDK_Down && todoModel.canDecrement(selectedItem)) {
|
||||||
|
todoModel.decrementPriority(selectedItem);
|
||||||
|
auto row = taskList.getRowAtIndex(idx + 1);
|
||||||
|
taskList.selectRow(row);
|
||||||
|
Widget rowWidget = cast(Widget) row;
|
||||||
|
rowWidget.grabFocus();
|
||||||
|
}
|
||||||
|
if (!ctrlDown && event.keyval == GdkKeysyms.GDK_Up && idx > 0) {
|
||||||
|
auto row = taskList.getRowAtIndex(idx - 1);
|
||||||
|
taskList.selectRow(row);
|
||||||
|
Widget rowWidget = cast(Widget) row;
|
||||||
|
rowWidget.grabFocus();
|
||||||
|
}
|
||||||
|
if (!ctrlDown && event.keyval == GdkKeysyms.GDK_Down && idx + 1 < todoModel.itemCount) {
|
||||||
|
auto row = taskList.getRowAtIndex(idx + 1);
|
||||||
|
taskList.selectRow(row);
|
||||||
|
Widget rowWidget = cast(Widget) row;
|
||||||
|
rowWidget.grabFocus();
|
||||||
|
}
|
||||||
|
if (event.keyval == GdkKeysyms.GDK_Delete) {
|
||||||
|
todoModel.removeItem(selectedPrio);
|
||||||
|
}
|
||||||
|
if (event.keyval == GdkKeysyms.GDK_Return) {
|
||||||
|
selectedItem.checked = !selectedItem.checked;
|
||||||
|
todoModel.notifyListeners();
|
||||||
|
auto row = taskList.getRowAtIndex(idx);
|
||||||
|
taskList.selectRow(row);
|
||||||
|
Widget rowWidget = cast(Widget) row;
|
||||||
|
rowWidget.grabFocus();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
extern (C) void addTask() {
|
extern (C) void addTask() {
|
||||||
todoModel.addItem(taskEntry.getText());
|
todoModel.addItem(taskEntry.getText());
|
||||||
taskEntry.setText("");
|
taskEntry.setText("");
|
||||||
|
@ -111,6 +179,12 @@ extern (C) void onWindowDestroy() {
|
||||||
Main.quit();
|
Main.quit();
|
||||||
if (todoModel.getOpenFilename() !is null) {
|
if (todoModel.getOpenFilename() !is null) {
|
||||||
todoModel.saveToJson(todoModel.getOpenFilename());
|
todoModel.saveToJson(todoModel.getOpenFilename());
|
||||||
|
string configPath = buildPath(getHomeDir(), ".config/todo-d/");
|
||||||
|
if (!exists(configPath)) {
|
||||||
|
mkdirRecurse(configPath);
|
||||||
|
}
|
||||||
|
string lastOpenFile = buildPath(configPath, "last-open.txt");
|
||||||
|
std.file.write(lastOpenFile, todoModel.getOpenFilename());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,3 +230,15 @@ extern (C) void onOpenMenuActivated() {
|
||||||
}
|
}
|
||||||
dialog.close();
|
dialog.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extern (C) void onAboutMenuActivated() {
|
||||||
|
import gtk.AboutDialog;
|
||||||
|
AboutDialog dialog = new AboutDialog();
|
||||||
|
dialog.setProgramName("Todo-D");
|
||||||
|
dialog.setLicenseType(GtkLicense.MIT_X11);
|
||||||
|
dialog.setAuthors(["Andrew Lalis"]);
|
||||||
|
dialog.setComments("A simple To-Do app written in D using GTK.");
|
||||||
|
dialog.setWebsite("https://github.com/andrewlalis/todo-d");
|
||||||
|
dialog.run();
|
||||||
|
dialog.destroy();
|
||||||
|
}
|
||||||
|
|
|
@ -13,14 +13,7 @@ class ToDoItem {
|
||||||
|
|
||||||
interface ModelUpdateListener {
|
interface ModelUpdateListener {
|
||||||
void itemsUpdated(ToDoItem[] items);
|
void itemsUpdated(ToDoItem[] items);
|
||||||
|
void fileUpdated(string filename);
|
||||||
static ModelUpdateListener of(void delegate(ToDoItem[]) dg) {
|
|
||||||
return new class ModelUpdateListener {
|
|
||||||
void itemsUpdated(ToDoItem[] items) {
|
|
||||||
dg(items);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ToDoModel {
|
class ToDoModel {
|
||||||
|
@ -36,6 +29,15 @@ class ToDoModel {
|
||||||
return openFilename;
|
return openFilename;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ToDoItem getItemAt(int prio) {
|
||||||
|
if (prio < 1 || prio > items.length) return null;
|
||||||
|
return items[prio - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong itemCount() {
|
||||||
|
return items.length;
|
||||||
|
}
|
||||||
|
|
||||||
void addItem(string text) {
|
void addItem(string text) {
|
||||||
addItem(new ToDoItem(text, 1_000_000, false));
|
addItem(new ToDoItem(text, 1_000_000, false));
|
||||||
}
|
}
|
||||||
|
@ -99,7 +101,10 @@ class ToDoModel {
|
||||||
);
|
);
|
||||||
items ~= item;
|
items ~= item;
|
||||||
}
|
}
|
||||||
|
if (openFilename != filename) {
|
||||||
openFilename = filename;
|
openFilename = filename;
|
||||||
|
foreach (l; listeners) l.fileUpdated(filename);
|
||||||
|
}
|
||||||
sort!((a, b) => a.priority < b.priority)(items);
|
sort!((a, b) => a.priority < b.priority)(items);
|
||||||
normalizePrio();
|
normalizePrio();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
@ -119,12 +124,16 @@ class ToDoModel {
|
||||||
}
|
}
|
||||||
j["items"] = JSONValue(itemObjs);
|
j["items"] = JSONValue(itemObjs);
|
||||||
write(filename, toJSON(j, true));
|
write(filename, toJSON(j, true));
|
||||||
|
if (openFilename != filename) {
|
||||||
openFilename = filename;
|
openFilename = filename;
|
||||||
|
foreach (l; listeners) l.fileUpdated(filename);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
items = [];
|
items = [];
|
||||||
openFilename = null;
|
openFilename = null;
|
||||||
|
foreach (l; listeners) l.fileUpdated(openFilename);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,7 +144,7 @@ class ToDoModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notifyListeners() {
|
public void notifyListeners() {
|
||||||
foreach (l; listeners) l.itemsUpdated(this.items);
|
foreach (l; listeners) l.itemsUpdated(this.items);
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue