diff --git a/gresource.xml b/gresource.xml new file mode 100644 index 0000000..89cf656 --- /dev/null +++ b/gresource.xml @@ -0,0 +1,6 @@ + + + + todo-ui.glade + + \ No newline at end of file diff --git a/source/app.d b/source/app.d index c73ff1b..9c9f19b 100644 --- a/source/app.d +++ b/source/app.d @@ -1,16 +1,99 @@ -import std.stdio; - import model; -import view : setupUI; +import std.stdio; import gtk.MainWindow; import gtk.Main; +import gtk.Builder; +import gtk.Application; +import gtk.ApplicationWindow; +import gtk.Entry; +import gtk.Box; +import gtk.ListBox; +import gtk.ListBoxRow; +import gtk.Label; +import gtk.CheckButton; +import gtk.ToggleButton; +import gtk.Button; + +class ToDoItemWidget : Box { + this(ToDoItem item, ToDoModel todoModel) { + super(GtkOrientation.HORIZONTAL, 5); + CheckButton button = new CheckButton(); + button.setActive(item.checked); + button.addOnToggled(delegate(ToggleButton b) { + item.checked = b.getActive(); + }); + Label label = new Label(item.text); + label.setLineWrap(true); + label.setHalign(GtkAlign.START); + label.setValign(GtkAlign.CENTER); + 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); + } +} + +Entry taskEntry; +ListBox taskList; + +ToDoModel todoModel; void main(string[] args) { Main.init(args); - MainWindow window = new MainWindow("To-Do"); - window.setDefaultSize(300, 500); - setupUI(window); + Builder builder = new Builder("todo-ui.glade"); + + builder.addCallbackSymbol("onAddTask", &addTask); + builder.addCallbackSymbol("onWindowDestroy", &onWindowDestroy); + builder.addCallbackSymbol("onQuitMenuActivated", &onWindowDestroy); + builder.connectSignals(null); + + taskList = cast(ListBox) builder.getObject("taskList"); + taskEntry = cast(Entry) builder.getObject("addTaskEntry"); + + todoModel = new ToDoModel(); + import std.functional : toDelegate; + todoModel.addListener(ModelUpdateListener.of(toDelegate(&itemsUpdated))); + + ApplicationWindow window = cast(ApplicationWindow) builder.getObject("window"); window.showAll(); Main.run(); } + +void itemsUpdated(ToDoItem[] items) { + taskList.removeAll(); + foreach (item; items) { + auto widget = new ToDoItemWidget(item, todoModel); + auto row = new ListBoxRow(); + row.add(widget); + taskList.add(row); + } + taskList.showAll(); +} + +extern (C) void addTask() { + todoModel.addItem(taskEntry.getText()); + taskEntry.setText(""); +} + +extern (C) void onWindowDestroy() { + Main.quit(); +} diff --git a/source/model.d b/source/model.d index ea01d83..f82ff98 100644 --- a/source/model.d +++ b/source/model.d @@ -1,6 +1,92 @@ module model; -struct ToDoItem { - string text; - int priority; +class ToDoItem { + string text; + int priority; + bool checked; + this(string text, int priority, bool checked) { + this.text = text; + this.priority = priority; + this.checked = checked; + } +} + +interface ModelUpdateListener { + void itemsUpdated(ToDoItem[] items); + + static ModelUpdateListener of(void delegate(ToDoItem[]) dg) { + return new class ModelUpdateListener { + void itemsUpdated(ToDoItem[] items) { + dg(items); + } + }; + } +} + +class ToDoModel { + private ToDoItem[] items; + private ModelUpdateListener[] listeners; + + void addListener(ModelUpdateListener listener) { + listeners ~= listener; + } + + void addItem(string text) { + addItem(new ToDoItem(text, 1_000_000, false)); + } + + void addItem(ToDoItem item) { + items ~= item; + normalizePrio(); + notifyListeners(); + } + + void removeItem(int prio) { + import std.algorithm; + size_t idx = prio - 1; // Assume prio is normalized to start at 1. + items = items.remove(idx); + normalizePrio(); + notifyListeners(); + } + + bool canIncrement(ToDoItem item) { + return item.priority > 1; + } + + void incrementPriority(ToDoItem item) { + size_t idx = item.priority - 1; + if (idx == 0) return; + ToDoItem higher = items[idx - 1]; + item.priority -= 1; + higher.priority += 1; + items[idx - 1] = item; + items[idx] = higher; + notifyListeners(); + } + + bool canDecrement(ToDoItem item) { + return item.priority < items.length; + } + + void decrementPriority(ToDoItem item) { + size_t idx = item.priority - 1; + if (idx + 1 >= items.length) return; + ToDoItem lower = items[idx + 1]; + item.priority += 1; + lower.priority -= 1; + items[idx + 1] = item; + items[idx] = lower; + notifyListeners(); + } + + private void normalizePrio() { + int prio = 1; + foreach (item; items) { + item.priority = prio++; + } + } + + private void notifyListeners() { + foreach (l; listeners) l.itemsUpdated(this.items); + } } \ No newline at end of file diff --git a/source/view.d b/source/view.d deleted file mode 100644 index b545e0a..0000000 --- a/source/view.d +++ /dev/null @@ -1,105 +0,0 @@ -module view; - -import model; - -import gtk.Box; -import gtk.ListBox; -import gtk.ListBoxRow; -import gtk.Label; -import gtk.CheckButton; -import gtk.Window; -import gtk.ScrolledWindow; -import gtk.Entry; -import gtk.Button; - -// Yes, I use global items for my application. - -ListBox todoList; -Entry todoEntry; - -ToDoItem[] items = []; - -void setupUI(Window w) { - todoList = new ListBox(); - todoEntry = new Entry(); - - auto vbox = new Box(GtkOrientation.VERTICAL, 5); - - todoList.setSelectionMode(GtkSelectionMode.NONE); - - vbox.packStart(new ScrolledWindow(todoList), true, true, 5); - - auto addBox = new Box(GtkOrientation.HORIZONTAL, 5); - addBox.packStart(todoEntry, true, true, 0); - Button addButton = new Button("Add"); - addButton.addOnClicked(delegate(Button b) {addItem();}); - addBox.packEnd(addButton, false, false, 0); - vbox.packEnd(addBox, false, false, 0); - - w.add(vbox); -} - -void addItem() { - import std.algorithm; - import std.string; - string text = todoEntry.getText().strip; - if (text.length == 0) return; - int maxPrio = -1_000_000; - foreach (item; items) { - maxPrio = max(maxPrio, item.priority); - } - ToDoItem newItem = ToDoItem(text, maxPrio + 1); - items ~= newItem; - todoEntry.setText(""); - normalizePriorities(); - refreshList(); -} - -void normalizePriorities() { - int prio = 1; - foreach (item; items) { - item.priority = prio++; - } -} - -void removeItem(int prio) { - import std.algorithm; - import std.array; - ToDoItem[] newItems = items.filter!(item => item.priority != prio).array; - items = newItems; - normalizePriorities(); - refreshList(); -} - -void refreshList() { - todoList.removeAll(); - foreach (item; items) { - auto widget = new ToDoItemWidget(item); - auto row = new ListBoxRow(); - row.add(widget); - todoList.add(row); - } - todoList.showAll(); -} - -class ToDoItemWidget : Box { - private ToDoItem item; - - this(ToDoItem item) { - super(GtkOrientation.HORIZONTAL, 5); - this.item = item; - CheckButton button = new CheckButton(); - Label label = new Label(item.text); - label.setLineWrap(true); - label.setHalign(GtkAlign.START); - label.setValign(GtkAlign.CENTER); - this.packStart(button, false, false, 0); - - Button removeButton = new Button("Remove"); - removeButton.addOnClicked(delegate(Button b) { - removeItem(item.priority); - }); - this.packEnd(removeButton, false, false, 0); - this.packEnd(label, true, true, 0); - } -} \ No newline at end of file diff --git a/todo-ui.glade b/todo-ui.glade new file mode 100644 index 0000000..1fafd2e --- /dev/null +++ b/todo-ui.glade @@ -0,0 +1,234 @@ + + + + + + False + center + + + + + + + True + False + vertical + + + True + False + + + True + False + _File + True + + + True + False + + + gtk-new + True + False + True + True + + + + + gtk-open + True + False + True + True + + + + + gtk-save + True + False + True + True + + + + + gtk-save-as + True + False + True + True + + + + + True + False + + + + + gtk-quit + True + False + True + True + + + + + + + + + + True + False + _Edit + True + + + True + False + + + gtk-cut + True + False + True + True + + + + + gtk-copy + True + False + True + True + + + + + gtk-paste + True + False + True + True + + + + + gtk-delete + True + False + True + True + + + + + + + + + True + False + _Help + True + + + True + False + + + gtk-about + True + False + True + True + + + + + + + + + False + True + 0 + + + + + True + True + in + + + True + False + + + True + False + none + + + + + + + True + True + 1 + + + + + True + False + + + True + True + + + True + True + 0 + + + + + gtk-add + True + True + True + True + True + + + + False + True + end + 1 + + + + + False + True + 2 + + + + + +