// bevel task timer
// 19.026
// vgmlr
// mlwrk
#include <gtkmm.h>
#include <libnotify/notify.h>
#include <iostream>
#include <iomanip>
#include <sstream>
#include <vector>
const std::string CSS =
"window { background-color: #EEEEEE; }"
".add-btn:hover { border-color: #BBBBBB; background-color:#FFFFFF; min-width: 12px; }"
"list, list row, list row:hover, list row:selected, list row:focus { background-color: transparent; outline: none; box-shadow: none; }"
"list row:first-child { margin-top: 2px; }"
"entry { border-color: #BBBBBB; font-family: Monospace; font-size: 0.9em; }"
"entry:focus { border-color: #BBBBBB; }"
"entry.active-entry { font-style: italic; }"
".atn-btn, .atn-btn:active { border-color: #BBBBBB; background-color: #FFFFFF; min-width: 12px; }"
".atn-btn:hover, .atn-btn-done { border-color: #BBBBBB; background-color: #EEEEEE; min-width: 12px; }"
"spinbutton button { border-color: #BBBBBB; }"
"spinbutton button:hover, spinbutton.spinbutton-done, spinbutton.spinbutton-done button, spinbutton.spinbutton-done entry { border-color: #BBBBBB; background-color: #EEEEEE; }"
".start-btn { border-color: #BBBBBB; background-color: #FFFFFF; min-width: 50px; }"
".start-btn:hover, .start-btn-done { border-color: #BBBBBB; background-color: #EEEEEE; min-width: 50px; }"
".remove-btn { border-color: #BBBBBB; min-width: 12px; }"
".remove-btn:hover, .remove-btn-done { border-color: #BBBBBB; background-color: #EEEEEE; min-width: 12px; }";
class NotificationHelper {
public:
NotificationHelper() {
notify_init("bevel");
}
void send_alert(const std::string& project_name, const std::string& next_project_name) {
NotifyNotification* n = notify_notification_new(
("Finished: " + project_name).c_str(),
("Next: " + next_project_name).c_str(),
"dialog-information"
);
notify_notification_set_urgency(n, NOTIFY_URGENCY_CRITICAL);
notify_notification_show(n, nullptr);
g_object_unref(G_OBJECT(n));
}
};
class tmr_item : public Gtk::ListBoxRow {
public:
sigc::signal<void, tmr_item*> signal_start_clicked;
sigc::signal<void, tmr_item*> signal_finished;
sigc::signal<void, tmr_item*, bool> signal_move;
sigc::signal<void, tmr_item*> signal_request_next_entry;
sigc::signal<void, tmr_item*> signal_request_prev_entry;
tmr_item(const std::string& name) :
sec_left(0), sec_start(0), is_running(false), p_box(Gtk::ORIENTATION_HORIZONTAL, 5) {
p_box.set_margin_top(5);
p_box.set_margin_start(5);
p_box.set_margin_end(5);
add(p_box);
up_btn.set_label("▲");
up_btn.get_style_context()->add_class("atn-btn");
up_btn.set_can_focus(false);
up_btn.signal_clicked().connect([this] { signal_move.emit(this, true); });
p_box.pack_start(up_btn, false, false, 0);
down_btn.set_label("▼");
down_btn.get_style_context()->add_class("atn-btn");
down_btn.set_can_focus(false);
down_btn.signal_clicked().connect([this] { signal_move.emit(this, false); });
p_box.pack_start(down_btn, false, false, 0);
entry.set_text(name);
entry.signal_key_press_event().connect(sigc::mem_fun(*this, &tmr_item::on_entry_key_press));
p_box.pack_start(entry, true, true, 0);
auto adj = Gtk::Adjustment::create(60, 1, 360, 1);
minutes_spin.set_adjustment(adj);
p_box.pack_start(minutes_spin, false, false, 0);
start_btn.set_label("Start");
start_btn.get_style_context()->add_class("start-btn");
start_btn.set_can_focus(false);
start_btn.signal_clicked().connect(sigc::mem_fun(*this, &tmr_item::toggle_timer));
p_box.pack_start(start_btn, false, false, 0);
remove_btn.set_label("✖");
remove_btn.get_style_context()->add_class("remove-btn");
remove_btn.set_can_focus(false);
remove_btn.signal_clicked().connect([this] {
auto parent = dynamic_cast<Gtk::ListBox*>(get_parent());
if (parent) parent->remove(*this);
});
p_box.pack_start(remove_btn, false, false, 0);
show_all_children();
}
Gtk::Entry* get_entry() { return &entry; }
std::string get_project_name() const { return entry.get_text(); }
std::string get_time_string() const {
int mins = sec_left / 60;
int secs = sec_left % 60;
std::stringstream ss;
ss << std::setfill('0') << std::setw(2) << mins << ":"
<< std::setfill('0') << std::setw(2) << secs;
return ss.str();
}
void set_entry_bold(bool bold) {
auto style_ctx = entry.get_style_context();
if (bold) {
style_ctx->add_class("active-entry");
} else {
style_ctx->remove_class("active-entry");
}
}
void update_entry_background() {
if (sec_start <= 0) return;
double percent = (sec_start - sec_left) / static_cast<double>(sec_start) * 100.0;
percent = std::max(0.0, std::min(100.0, percent));
std::stringstream ss;
ss << std::fixed << std::setprecision(1) << percent;
std::string percent_str = ss.str();
std::string gradient = "entry { background: linear-gradient(to right, #EEEEEE 0%, #EEEEEE "
+ percent_str + "%, #FFFFFF " + percent_str + "%, #FFFFFF 100%); }";
auto css_set = Gtk::CssProvider::create();
css_set->load_from_data(gradient);
entry.get_style_context()->add_provider(css_set, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
}
void toggle_timer() {
if (!is_running) {
if (sec_left <= 0) {
sec_left = (int)minutes_spin.get_value() * 60;
sec_start = sec_left;
update_entry_background();
}
start_timer();
signal_start_clicked.emit(this);
} else {
stop_timer();
}
}
void start_timer() {
is_running = true;
start_btn.set_label("Pause");
timer_conn = Glib::signal_timeout().connect(sigc::mem_fun(*this, &tmr_item::tick), 1000);
}
void stop_timer() {
is_running = false;
start_btn.set_label("Start");
timer_conn.disconnect();
}
bool tick() {
if (!is_running) return false;
sec_left--;
update_entry_background();
if (sec_left <= 0) {
stop_timer();
start_btn.set_label("Done");
up_btn.get_style_context()->add_class("atn-btn-done");
down_btn.get_style_context()->add_class("atn-btn-done");
minutes_spin.get_style_context()->add_class("spinbutton-done");
start_btn.get_style_context()->add_class("start-btn-done");
remove_btn.get_style_context()->add_class("remove-btn-done");
signal_finished.emit(this);
return false;
}
return true;
}
bool running() const { return is_running; }
protected:
bool on_entry_key_press(GdkEventKey* event) {
if (event->keyval == GDK_KEY_Tab) {
if (event->state & Gdk::SHIFT_MASK) {
signal_request_prev_entry.emit(this);
} else {
signal_request_next_entry.emit(this);
}
return true;
}
return false;
}
private:
int sec_left;
int sec_start;
bool is_running;
sigc::connection timer_conn;
Gtk::Box p_box;
Gtk::Button up_btn;
Gtk::Button down_btn;
Gtk::Entry entry;
Gtk::SpinButton minutes_spin;
Gtk::Button start_btn;
Gtk::Button remove_btn;
};
class BevelApp : public Gtk::Window {
public:
BevelApp() : active_item(nullptr), vbox(Gtk::ORIENTATION_VERTICAL, 10) {
set_title("bevel");
set_default_size(550, 450);
auto css_insert = Gtk::CssProvider::create();
css_insert->load_from_data(CSS);
Gtk::StyleContext::add_provider_for_screen(Gdk::Screen::get_default(), css_insert, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
add(vbox);
Gtk::HeaderBar* header = Gtk::make_managed<Gtk::HeaderBar>();
header->set_title("bevel");
header->set_show_close_button(true);
set_titlebar(*header);
Gtk::Button* add_btn = Gtk::make_managed<Gtk::Button>("Add Project");
add_btn->signal_clicked().connect(sigc::mem_fun(*this, &BevelApp::on_add_project));
header->pack_start(*add_btn);
listbox.set_selection_mode(Gtk::SELECTION_NONE);
scrolled_window.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
scrolled_window.add(listbox);
vbox.pack_start(scrolled_window, true, true, 0);
Glib::signal_timeout().connect(sigc::mem_fun(*this, &BevelApp::update_title), 1000);
show_all_children();
}
private:
void on_add_project() {
auto item = Gtk::make_managed<tmr_item>("");
item->signal_start_clicked.connect(sigc::mem_fun(*this, &BevelApp::on_start_selected));
item->signal_finished.connect(sigc::mem_fun(*this, &BevelApp::on_timer_finished));
item->signal_move.connect(sigc::mem_fun(*this, &BevelApp::on_move_item));
item->signal_request_next_entry.connect(sigc::mem_fun(*this, &BevelApp::on_request_next_entry));
item->signal_request_prev_entry.connect(sigc::mem_fun(*this, &BevelApp::on_request_prev_entry));
listbox.add(*item);
item->show();
}
void on_move_item(tmr_item* item, bool move_up) {
int current_pos = item->get_index();
int new_pos = move_up ? current_pos - 1 : current_pos + 1;
auto chidlen = listbox.get_children();
if (new_pos >= 0 && new_pos < (int)chidlen.size()) {
listbox.remove(*item);
listbox.insert(*item, new_pos);
}
}
void on_request_next_entry(tmr_item* current_item) {
int current_pos = current_item->get_index();
auto chidlen = listbox.get_children();
if (current_pos + 1 < (int)chidlen.size()) {
auto next_row = dynamic_cast<tmr_item*>(chidlen[current_pos + 1]);
if (next_row) {
next_row->get_entry()->grab_focus();
}
}
}
void on_request_prev_entry(tmr_item* current_item) {
int current_pos = current_item->get_index();
if (current_pos - 1 >= 0) {
auto chidlen = listbox.get_children();
auto prev_row = dynamic_cast<tmr_item*>(chidlen[current_pos - 1]);
if (prev_row) {
prev_row->get_entry()->grab_focus();
}
}
}
void on_start_selected(tmr_item* item) {
if (active_item && active_item != item) {
active_item->stop_timer();
active_item->set_entry_bold(false);
}
active_item = item;
active_item->set_entry_bold(true);
}
void on_timer_finished(tmr_item* finished_item) {
std::string next_name = "No more projects";
auto chidlen = listbox.get_children();
for (size_t i = 0; i < chidlen.size(); ++i) {
if (chidlen[i] == finished_item && i + 1 < chidlen.size()) {
auto next_row = dynamic_cast<tmr_item*>(chidlen[i+1]);
if (next_row) next_name = next_row->get_project_name();
break;
}
}
notifier.send_alert(finished_item->get_project_name(), next_name);
finished_item->set_entry_bold(false);
active_item = nullptr;
set_title("bevel");
}
bool update_title() {
if (active_item && active_item->running()) {
set_title(active_item->get_time_string() + " - " + active_item->get_project_name());
}
return true;
}
tmr_item* active_item;
NotificationHelper notifier;
Gtk::Box vbox;
Gtk::ListBox listbox;
Gtk::ScrolledWindow scrolled_window;
};
int main(int argc, char* argv[]) {
auto app = Gtk::Application::create(argc, argv, "com.vgmlr.bevel");
BevelApp window;
return app->run(window);
}