Pith - bevel
bevel/bevel.cpp [11.8 kb]
Modified: 23:11:09 55 026 (13 May 026)
17 Days Ago
// 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);
}
Updates
Shim - Android 70.026.1
Wedge - Linux 68.026.1
Wedge - Android 68.026.1
Taper - Linux 64.026.1
Ayh Extension - Chrome 63.026.1
Dev
TVShow (227) 'CSA'
TVShow (228) 'APT'
TVProgram (83) 'BXT'
Miter Update(s)
Shim (Dictation)

Menu
Calendar
Project Tin (024/029)
Miter
RSS Feed
User Avatar
@vgmlr
=SUM(parts)