Add some configs from generic Ubuntu

This commit is contained in:
2020-05-11 05:16:27 -04:00
parent f32c1048d1
commit 754f64f135
16037 changed files with 205635 additions and 137 deletions

View File

@@ -0,0 +1,73 @@
var DNDGroup = {
TASK : 'TASK',
KANBAN_COLUMN : 'KANBAN_COLUMN',
};
var SortOrder = {
ASCENDING : 'ASCENDING',
DESCENDING : 'DESCENDING',
};
var SortType = {
PIN : 'PIN',
CONTEXT : 'CONTEXT',
PROJECT : 'PROJECT',
PRIORITY : 'PRIORITY',
DUE_DATE : 'DUE_DATE',
ALPHABET : 'ALPHABET',
RECURRENCE : 'RECURRENCE',
COMPLETED : 'COMPLETED',
CREATION_DATE : 'CREATION_DATE',
COMPLETION_DATE : 'COMPLETION_DATE',
};
var View = {
CLEAR : 'CLEAR',
STATS : 'STATS',
SEARCH : 'SEARCH',
EDITOR : 'EDITOR',
DEFAULT : 'DEFAULT',
LOADING : 'LOADING',
SELECT_SORT : 'SELECT_SORT',
FILE_SWITCH : 'FILE_SWITCH',
SELECT_FILTER : 'SELECT_FILTER',
KANBAN_SWITCHER : 'KANBAN_SWITCHER',
};
var SORT_RECORD = () => [
[SortType.PIN , SortOrder.DESCENDING],
[SortType.COMPLETED , SortOrder.ASCENDING],
[SortType.PRIORITY , SortOrder.ASCENDING],
[SortType.DUE_DATE , SortOrder.ASCENDING],
[SortType.RECURRENCE , SortOrder.ASCENDING],
[SortType.CONTEXT , SortOrder.ASCENDING],
[SortType.PROJECT , SortOrder.ASCENDING],
[SortType.CREATION_DATE , SortOrder.ASCENDING],
[SortType.COMPLETION_DATE , SortOrder.ASCENDING],
[SortType.ALPHABET , SortOrder.ASCENDING],
];
var FILTER_RECORD = () => ({
invert_filters : false,
deferred : false,
recurring : false,
hidden : false,
completed : false,
no_priority : false,
priorities : [],
contexts : [],
projects : [],
custom : [],
custom_active : [],
});
var TODO_RECORD = () => ({
name : "",
active : false,
todo_file : "", // (file path)
done_file : "", // (file path or "")
time_tracker_dir : "", // (file path or "")
automatic_sort : false,
filters : FILTER_RECORD(),
sorts : SORT_RECORD(),
});

View File

@@ -0,0 +1,908 @@
const St = imports.gi.St;
const Gio = imports.gi.Gio
const Gtk = imports.gi.Gtk;
const GLib = imports.gi.GLib;
const Clutter = imports.gi.Clutter;
const GnomeDesktop = imports.gi.GnomeDesktop;
const Main = imports.ui.main;
const ByteArray = imports.byteArray;
const Signals = imports.signals;
const Mainloop = imports.mainloop;
const ME = imports.misc.extensionUtils.getCurrentExtension();
const Gettext = imports.gettext.domain(ME.metadata['gettext-domain']);
const _ = Gettext.gettext;
const ngettext = Gettext.ngettext;
const SIG_MANAGER = ME.imports.lib.signal_manager;
const KEY_MANAGER = ME.imports.lib.keybinding_manager;
const MISC_UTILS = ME.imports.lib.misc_utils;
const G = ME.imports.sections.todo.GLOBAL;
const TASK = ME.imports.sections.todo.task_item;
const VIEW_MANAGER = ME.imports.sections.todo.view_manager;
const TIME_TRACKER = ME.imports.sections.todo.time_tracker;
const VIEW_STATS = ME.imports.sections.todo.view__stats;
const VIEW_CLEAR = ME.imports.sections.todo.view__clear_tasks;
const VIEW_SORT = ME.imports.sections.todo.view__sort;
const VIEW_DEFAULT = ME.imports.sections.todo.view__default;
const VIEW_SEARCH = ME.imports.sections.todo.view__search;
const VIEW_LOADING = ME.imports.sections.todo.view__loading;
const VIEW_FILTERS = ME.imports.sections.todo.view__filters;
const VIEW_TASK_EDITOR = ME.imports.sections.todo.view__task_editor;
const VIEW_FILE_SWITCHER = ME.imports.sections.todo.view__file_switcher;
const VIEW_KANBAN_SWITCHER = ME.imports.sections.todo.view__kanban_switcher;
const CACHE_FILE = '~/.cache/timepp_gnome_shell_extension/timepp_todo.json';
// =====================================================================
// @@@ Main
//
// @ext : obj (main extension object)
// @settings : obj (extension settings)
//
// @signals:
// - 'new-day' (new day started) (returns string in yyyy-mm-dd iso format)
// - 'tasks-changed'
// =====================================================================
var SectionMain = class SectionMain extends ME.imports.sections.section_base.SectionBase {
constructor (section_name, ext, settings) {
super(section_name, ext, settings);
this.actor.add_style_class_name('todo-section');
this.separate_menu = this.settings.get_boolean('todo-separate-menu');
this.cache_file = null;
this.cache = null;
this.sigm = new SIG_MANAGER.SignalManager();
this.keym = new KEY_MANAGER.KeybindingManager(this.settings);
this.time_tracker = null;
this.view_manager = new VIEW_MANAGER.ViewManager(this.ext, this);
// The view manager only allows one view to be visible at a time; however,
// since the stats view uses the fullscreen iface, it is orthogonal to
// the other views, so we don't use the view manager for it.
this.stats_view = new VIEW_STATS.StatsView(this.ext, this, 0);
//
// init cache file
//
try {
this.cache_file = MISC_UTILS.file_new_for_path(CACHE_FILE);
let cache_format_version =
ME.metadata['cache-file-format-version'].todo;
if (this.cache_file.query_exists(null)) {
let [, contents] = this.cache_file.load_contents(null);
this.cache = JSON.parse(ByteArray.toString(contents));
}
if (!this.cache || !this.cache.format_version ||
this.cache.format_version !== cache_format_version) {
this.cache = {
format_version: cache_format_version,
// array [of G.TODO_RECORD]
todo_files: [],
};
}
} catch (e) {
logError(e);
return;
}
this.create_tasks_mainloop_id = null;
// We use this for tracking when a new day begins.
this.wallclock = new GnomeDesktop.WallClock();
// Track how many tasks have a particular proj/context/prio, a
this.stats = null;
this._reset_stats_obj();
// ref to current todo record in cache file
this.current_todo_file = null;
// A GFile to the todo.txt file, GMonitor.
this.todo_txt_file = null;
this.todo_file_monitor = null;
// All task objects.
this.tasks = [];
//
// keybindings
//
this.keym.add('todo-keybinding-open', () => {
this.ext.open_menu(this.section_name);
this.show_view__default();
});
this.keym.add('todo-keybinding-open-to-add', () => {
this.ext.open_menu(this.section_name);
this.show_view__task_editor();
});
this.keym.add('todo-keybinding-open-to-search', () => {
this.ext.open_menu(this.section_name);
this.show_view__search();
});
this.keym.add('todo-keybinding-open-to-stats', () => {
this.show_view__time_tracker_stats();
});
this.keym.add('todo-keybinding-open-to-switch-files', () => {
this.ext.open_menu(this.section_name);
this.show_view__file_switcher();
});
this.keym.add('todo-keybinding-open-todotxt-file', () => {
if (! this.todo_txt_file) return;
let path = this.todo_txt_file.get_path();
if (path) MISC_UTILS.open_file_path(path);
});
//
// panel item
//
this.panel_item.actor.add_style_class_name('todo-panel-item');
this.panel_item.icon.gicon = MISC_UTILS.getIcon('timepp-todo-symbolic');
this._toggle_panel_item_mode();
//
// listen
//
this.sigm.connect(this.settings, 'changed::todo-separate-menu', () => {
this.separate_menu = this.settings.get_boolean('todo-separate-menu');
this.ext.update_panel_items();
});
this.sigm.connect(this.settings, 'changed::todo-task-width', () => {
let width = this.settings.get_int('todo-task-width');
for (let task of this.tasks) task.actor.width = width;
});
this.sigm.connect(this.wallclock, 'notify::clock', () => {
let t = GLib.DateTime.new_now(this.wallclock.timezone);
t = t.format('%H:%M');
if (t === '00:00') this._on_new_day_started();
});
this.sigm.connect(this.settings, 'changed::todo-panel-mode', () => this._toggle_panel_item_mode());
this.sigm.connect(this.ext, 'custom-css-changed', () => this._on_custom_css_changed());
//
// finally
//
this._init_todo_file();
}
disable_section () {
if (this.create_tasks_mainloop_id) {
Mainloop.source_remove(this.create_tasks_mainloop_id);
this.create_tasks_mainloop_id = null;
}
if (this.time_tracker) {
this.time_tracker.close();
this.time_tracker = null;
}
if (this.stats_view) {
this.stats_view.destroy();
this.stats_view = null;
}
this._disable_todo_file_monitor();
this.sigm.clear();
this.keym.clear();
this.view_manager.close_current_view();
this.view_manager = null;
this.tasks = [];
super.disable_section();
}
_init_todo_file () {
this.show_view__loading(true);
this.view_manager.lock = true;
// reset
{
if (this.create_tasks_mainloop_id) {
Mainloop.source_remove(this.create_tasks_mainloop_id);
this.create_tasks_mainloop_id = null;
}
if (this.time_tracker) {
this.time_tracker.close();
this.time_tracker = null;
}
if (this.todo_file_monitor) {
this.todo_file_monitor.cancel();
this.todo_file_monitor = null;
}
this.stats.priorities.clear();
this.stats.contexts.clear();
this.stats.projects.clear();
}
try {
if (this.cache.todo_files.length === 0) {
this.show_view__file_switcher(true);
this.view_manager.lock = true;
return;
}
this.current_todo_file = null;
let current = this.get_current_todo_file();
if (!current) {
this.show_view__file_switcher(true);
this.view_manager.lock = true;
return;
}
this.todo_txt_file = MISC_UTILS.file_new_for_path(current.todo_file);
if (! this.todo_txt_file.query_exists(null)) this.todo_txt_file.create(Gio.FileCreateFlags.NONE, null);
this._enable_todo_file_monitor();
} catch (e) {
this.show_view__file_switcher(true);
this.view_manager.lock = true;
logError(e);
Main.notify(_('Unable to load todo file'));
return;
}
let [, lines] = this.todo_txt_file.load_contents(null);
lines = ByteArray.toString(lines).split(/\r?\n/).filter((l) => /\S/.test(l));
this.create_tasks(lines, () => {
let needs_write = this._check_dates();
this.on_tasks_changed(needs_write);
this.time_tracker = new TIME_TRACKER.TimeTracker(this.ext, this);
});
}
_disable_todo_file_monitor () {
if (this.todo_file_monitor) {
this.todo_file_monitor.cancel();
this.todo_file_monitor = null;
}
}
_enable_todo_file_monitor () {
[this.todo_file_monitor,] =
MISC_UTILS.file_monitor(this.todo_txt_file, () => this._on_todo_file_changed());
}
store_cache () {
if (! this.cache_file) return;
if(! this.cache_file.query_exists(null))
this.cache_file.create(Gio.FileCreateFlags.NONE, null);
this.cache_file.replace_contents(JSON.stringify(this.cache, null, 2),
null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, null);
}
get_current_todo_file () {
if (this.current_todo_file) return this.current_todo_file;
for (let it of this.cache.todo_files) {
if (it.active) {
this.current_todo_file = it;
break;
}
}
return this.current_todo_file;
}
write_tasks_to_file () {
this._disable_todo_file_monitor();
let content = '';
for (let it of this.tasks) content += it.task_str + '\n';
this.todo_txt_file.replace_contents(content, null, false,
Gio.FileCreateFlags.REPLACE_DESTINATION, null);
this._enable_todo_file_monitor();
}
_on_todo_file_changed (event_type) {
this._init_todo_file();
}
_on_new_day_started () {
this.emit('new-day', MISC_UTILS.date_yyyymmdd());
if (this._check_dates()) this.on_tasks_changed(true, true);
}
_check_dates () {
let today = MISC_UTILS.date_yyyymmdd();
let tasks_updated = false;
let recurred_tasks = 0;
let deferred_tasks = 0;
for (let task of this.tasks) {
if (task.check_recurrence()) {
tasks_updated = true;
recurred_tasks++;
}
if (task.check_deferred_tasks(today)) {
tasks_updated = true;
deferred_tasks++;
}
task.update_dates_markup();
}
if (tasks_updated) {
if (recurred_tasks > 0) {
Main.notify(ngettext('%d task has recurred',
'%d tasks have recurred',
recurred_tasks).format(recurred_tasks));
}
if (deferred_tasks > 0) {
Main.notify(ngettext('%d deferred task has been opened',
'%d deferred tasks have been opened',
deferred_tasks).format(deferred_tasks));
}
}
return tasks_updated;
}
_on_custom_css_changed () {
for (let task of this.tasks) {
task.update_body_markup();
task.update_dates_markup();
}
}
// The maps have the structure:
// @key : string (a context/project/priority)
// @val : natural (number of tasks that have that @key)
_reset_stats_obj () {
this.stats = {
deferred_tasks : 0,
recurring_completed : 0,
recurring_incompleted : 0,
hidden : 0,
completed : 0,
no_priority : 0,
priorities : new Map(),
contexts : new Map(),
projects : new Map(),
};
}
_toggle_panel_item_mode () {
if (this.settings.get_enum('todo-panel-mode') === 0)
this.panel_item.set_mode('icon');
else if (this.settings.get_enum('todo-panel-mode') === 1)
this.panel_item.set_mode('text');
else
this.panel_item.set_mode('icon_text');
}
// Create task objects from the given task strings and add them to the
// this.tasks array.
//
// Make sure to call this.on_tasks_changed() soon after calling this func.
//
// @todo_strings : array (of strings; each string is a line in todo.txt file)
// @callback : func
create_tasks (todo_strings, callback) {
if (this.create_tasks_mainloop_id) {
Mainloop.source_remove(this.create_tasks_mainloop_id);
this.create_tasks_mainloop_id = null;
}
// Since we are reusing already instantiated objects, get rid of any
// excess task object.
//
// @NOTE Reusing old objects can be the source of evil...
{
let len = todo_strings.length;
while (this.tasks.length > len) this.tasks.pop().actor.destroy();
}
this.create_tasks_mainloop_id = Mainloop.idle_add(() => {
this._create_tasks__finish(0, todo_strings, callback);
});
}
_create_tasks__finish (i, todo_strings, callback) {
if (i === todo_strings.length) {
if (typeof(callback) === 'function') callback();
this.create_tasks_mainloop_id = null;
return;
}
let str = todo_strings[i];
if (this.tasks[i])
this.tasks[i].reset(false, str);
else
this.tasks.push(new TASK.TaskItem(this.ext, this, str, false));
this.create_tasks_mainloop_id = Mainloop.idle_add(() => {
this._create_tasks__finish(++i, todo_strings, callback);
});
}
on_tasks_changed (write_to_file = true, refresh_default_view = false) {
//
// Update stats obj
//
{
this._reset_stats_obj();
let n, proj, context;
for (let task of this.tasks) {
if (task.is_deferred) {
this.stats.deferred_tasks++;
continue;
}
if (task.completed) {
if (task.rec_str) this.stats.recurring_completed++
else this.stats.completed++;
continue;
}
for (proj of task.projects) {
n = this.stats.projects.get(proj);
this.stats.projects.set(proj, n ? ++n : 1);
}
for (context of task.contexts) {
n = this.stats.contexts.get(context);
this.stats.contexts.set(context, n ? ++n : 1);
}
if (task.hidden) {
this.stats.hidden++;
continue;
}
if (task.priority === '(_)') {
this.stats.no_priority++;
} else {
n = this.stats.priorities.get(task.priority);
this.stats.priorities.set(task.priority, n ? ++n : 1);
}
if (task.rec_str) this.stats.recurring_incompleted++;
}
}
//
// update panel label
//
{
let n_incompleted = this.tasks.length -
this.stats.completed -
this.stats.hidden -
this.stats.recurring_completed -
this.stats.deferred_tasks;
this.panel_item.set_label('' + n_incompleted);
if (n_incompleted) this.panel_item.actor.remove_style_class_name('done');
else this.panel_item.actor.add_style_class_name('done');
}
//
// Since contexts/projects/priorities are filters, it can happen that we
// have redundant filters in case tasks were deleted. Clean 'em up.
//
{
let current = this.get_current_todo_file();
let i, arr, len;
arr = current.filters.priorities;
for (i = 0, len = arr.length; i < len; i++) {
if (! this.stats.priorities.has(arr[i])) {
arr.splice(i, 1);
len--; i--;
}
}
arr = current.filters.contexts;
for (i = 0, len = arr.length; i < len; i++) {
if (! this.stats.contexts.has(arr[i])) {
arr.splice(i, 1);
len--; i--;
}
}
arr = current.filters.projects;
for (i = 0, len = arr.length; i < len; i++) {
if (! this.stats.projects.has(arr[i])) {
arr.splice(i, 1);
len--; i--;
}
}
}
this.sort_tasks();
this.show_view__default(true, refresh_default_view);
if (write_to_file) this.write_tasks_to_file();
this.emit('tasks-changed');
}
sort_tasks () {
if (! this.get_current_todo_file().automatic_sort) return;
let property_map = {
[G.SortType.PIN] : 'pinned',
[G.SortType.CONTEXT] : 'first_context',
[G.SortType.PROJECT] : 'first_project',
[G.SortType.PRIORITY] : 'priority',
[G.SortType.COMPLETED] : 'completed',
[G.SortType.DUE_DATE] : 'due_date',
[G.SortType.ALPHABET] : 'msg_text',
[G.SortType.RECURRENCE] : 'rec_next',
[G.SortType.CREATION_DATE] : 'creation_date',
[G.SortType.COMPLETION_DATE] : 'completion_date',
};
let sort = this.get_current_todo_file().sorts;
let i = 0;
let len = sort.length;
let props = Array(len);
for (; i < len; i++) {
props[i] = property_map[ sort[i][0] ];
}
this.tasks.sort((a, b) => {
let x, y;
for (i = 0; (i < len) && (x = a[props[i]]) === (y = b[props[i]]); i++);
if (i === len) return 0;
switch (sort[i][0]) {
case G.SortType.PRIORITY:
if (sort[i][1] === G.SortOrder.DESCENDING) return +(x > y) || +(x === y) - 1;
else return +(x < y) || +(x === y) - 1;
default:
if (sort[i][1] === G.SortOrder.DESCENDING) return +(x < y) || +(x === y) - 1;
else return +(x > y) || +(x === y) - 1;
}
});
}
// Append the task strings of each given task to the current done.txt file.
//
// If a given task is not completed, it's task string will be updated to
// show that it's completed prior to been appended to the done.txt file.
//
// The task objects will not be changed.
//
// @tasks: array (of task objects)
archive_tasks (tasks) {
let content = '';
let today = MISC_UTILS.date_yyyymmdd();
for (let task of tasks) {
if (task.completed) {
content += task.task_str + '\n';
} else if (task.priority === '(_)') {
content += `x ${today} ${task.task_str}\n`;
} else {
content += `x ${today} ${task.task_str.slice(3)} pri:${task.priority[1]}\n`;
}
}
try {
let current = this.get_current_todo_file();
if (!current || !current.done_file) return;
let done_file = MISC_UTILS.file_new_for_path(current.done_file);
let append_stream = done_file.append_to(Gio.FileCreateFlags.NONE, null);
append_stream.write_all(content, null);
} catch (e) { logError(e); }
}
show_view__default (unlock = false, force_refresh = false) {
if (unlock) this.view_manager.lock = false;
else if (this.view_manager.lock) return;
if (!force_refresh && this.view_manager.current_view_name === G.View.DEFAULT) {
Mainloop.idle_add(() => this.view_manager.current_view.dummy_focus_actor.grab_key_focus());
return;
}
this.view_manager.close_current_view();
let view = new VIEW_DEFAULT.ViewDefault(this.ext, this);
this.view_manager.show_view({
view : view,
view_name : G.View.DEFAULT,
actors : [view.actor],
focused_actor : view.dummy_focus_actor,
close_callback : () => view.close(),
});
}
show_view__time_tracker_stats (task) {
if (! this.time_tracker) return;
this.ext.menu.close();
this.stats_view.open();
if (this.time_tracker.stats_data.size === 0)
this.stats_view.show_mode__banner(_('Loading...'));
Mainloop.idle_add(() => {
let stats = this.time_tracker.get_stats();
if (!stats) {
this.stats_view.show_mode__banner(_('Nothing found.'));
} else if (!task) {
this.stats_view.set_stats(...stats);
this.stats_view.show_mode__global(MISC_UTILS.date_yyyymmdd());
} else {
this.stats_view.set_stats(...stats);
let d = new Date();
this.stats_view.show_mode__single(d.getFullYear(), d.getMonth(), task.task_str, '()');
}
});
}
show_view__loading (unlock = false) {
if (unlock) this.view_manager.lock = false;
else if (this.view_manager.lock) return;
this.panel_item.set_mode('icon');
this.panel_item.actor.remove_style_class_name('done');
this.panel_item.icon.gicon = MISC_UTILS.getIcon('timepp-todo-loading-symbolic');
let view = new VIEW_LOADING.ViewLoading(this.ext, this);
this.view_manager.show_view({
view : view,
view_name : G.View.LOADING,
actors : [view.actor],
focused_actor : view.loading_msg,
close_callback : () => {
view.close();
this.panel_item.icon.gicon = MISC_UTILS.getIcon('timepp-todo-symbolic');
this._toggle_panel_item_mode();
}
});
}
show_view__search (search_str = false, unlock = false) {
if (unlock) this.view_manager.lock = false;
else if (this.view_manager.lock) return;
this.view_manager.close_current_view();
let view = new VIEW_SEARCH.ViewSearch(this.ext, this);
this.view_manager.show_view({
view : view,
view_name : G.View.SEARCH,
focused_actor : view.search_entry,
actors : [view.actor],
close_callback : () => view.close(),
});
if (search_str) view.search_entry.text = search_str;
}
show_view__kanban_switcher (unlock = false) {
if (unlock) this.view_manager.lock = false;
else if (this.view_manager.lock) return;
let view = new VIEW_KANBAN_SWITCHER.KanbanSwitcher(this.ext, this);
this.view_manager.show_view({
view : view,
view_name : G.View.KANBAN_SWITCHER,
actors : [view.actor],
focused_actor : view.entry,
close_callback : () => view.close(),
});
}
show_view__clear_completed (unlock = false) {
if (unlock) this.view_manager.lock = false;
else if (this.view_manager.lock) return;
let view = new VIEW_CLEAR.ViewClearTasks(this.ext, this);
this.view_manager.show_view({
view : view,
view_name : G.View.CLEAR,
actors : [view.actor],
focused_actor : view.button_cancel,
close_callback : () => view.close(),
});
view.connect('delete-all', () => {
let incompleted_tasks = [];
for (let i = 0, len = this.tasks.length; i < len; i++) {
if (!this.tasks[i].completed || this.tasks[i].rec_str)
incompleted_tasks.push(this.tasks[i]);
}
this.tasks = incompleted_tasks;
this.on_tasks_changed();
});
view.connect('archive-all', () => {
let completed_tasks = [];
let incompleted_tasks = [];
for (let task of this.tasks) {
if (!task.completed || task.rec_str) incompleted_tasks.push(task);
else completed_tasks.push(task);
}
this.archive_tasks(completed_tasks);
this.tasks = incompleted_tasks;
this.on_tasks_changed();
});
view.connect('cancel', () => {
this.show_view__default();
});
}
show_view__file_switcher (unlock = false) {
if (unlock) this.view_manager.lock = false;
else if (this.view_manager.lock) return;
let view = new VIEW_FILE_SWITCHER.ViewFileSwitcher(this.ext, this);
this.view_manager.show_view({
view : view,
view_name : G.View.FILE_SWITCH,
actors : [view.actor],
focused_actor : this.cache.todo_files.length ? view.entry : view.button_add_file,
close_callback : () => view.close(),
});
if (this.cache.todo_files.length === 0) this.panel_item.set_mode('icon');
view.connect('update', (_, files) => {
this.cache.todo_files = files;
this.store_cache();
Main.panel.menuManager.ignoreRelease();
this._init_todo_file();
});
view.connect('cancel', () => {
this.show_view__default();
});
}
show_view__sort (unlock = false) {
if (unlock) this.view_manager.lock = false;
else if (this.view_manager.lock) return;
let view = new VIEW_SORT.ViewSort(this.ext, this);
this.view_manager.show_view({
view : view,
view_name : G.View.SELECT_SORT,
actors : [view.actor],
focused_actor : view.button_ok,
close_callback : () => view.close(),
});
view.connect('update-sort', (_, new_sort, automatic_sort) => {
let current = this.get_current_todo_file();
current.sorts = new_sort;
current.automatic_sort = automatic_sort;
this.sort_tasks();
this.store_cache();
this.show_view__default();
});
}
show_view__filters (unlock = false) {
if (unlock) this.view_manager.lock = false;
else if (this.view_manager.lock) return;
let view = new VIEW_FILTERS.ViewFilters(this.ext, this);
this.view_manager.show_view({
view : view,
view_name : G.View.SELECT_FILTER,
actors : [view.actor],
focused_actor : view.entry.entry,
close_callback : () => view.close(),
});
view.connect('filters-updated', (_, filters) => {
this.get_current_todo_file().filters = filters;
this.store_cache();
this.show_view__default();
});
}
show_view__task_editor (task, unlock = false) {
if (unlock) this.view_manager.lock = false;
else if (this.view_manager.lock) return;
this.view_manager.lock = true;
let view = new VIEW_TASK_EDITOR.ViewTaskEditor(this.ext, this, task);
this.view_manager.show_view({
view : view,
view_name : G.View.EDITOR,
actors : [view.actor],
focused_actor : view.entry.entry,
close_callback : () => view.close(),
});
if (task) this.time_tracker.stop_tracking(task);
view.connect('delete-task', (_, do_archive) => {
if (do_archive) this.archive_tasks([task]);
for (let i = 0, len = this.tasks.length; i < len; i++) {
if (this.tasks[i] === task) {
this.tasks.splice(i, 1);
break;
}
}
this.on_tasks_changed();
});
view.connect('add-task', (_, task) => {
this.tasks.push(task);
this.on_tasks_changed();
});
view.connect('edited-task', () => {
this.on_tasks_changed();
});
view.connect('cancel', () => {
this.show_view__default(true);
});
}
}
Signals.addSignalMethods(SectionMain.prototype);

View File

@@ -0,0 +1,836 @@
const Gio = imports.gi.Gio
const GLib = imports.gi.GLib;
const Shell = imports.gi.Shell;
const Main = imports.ui.main;
const Util = imports.misc.util;
const ByteArray = imports.byteArray;
const Signals = imports.signals;
const Mainloop = imports.mainloop;
const ME = imports.misc.extensionUtils.getCurrentExtension();
const Gettext = imports.gettext.domain(ME.metadata['gettext-domain']);
const _ = Gettext.gettext;
const ngettext = Gettext.ngettext;
const IFACE = `${ME.path}/dbus/time_tracker_iface.xml`;
const MISC_UTILS = ME.imports.lib.misc_utils;
const REG = ME.imports.lib.regex;
const G = ME.imports.sections.todo.GLOBAL;
// =====================================================================
// @@@ TimeTracker
//
// @ext : obj (main extension object)
// @delegate : obj (main section object)
// =====================================================================
var TimeTracker = class TimeTracker {
constructor (ext, delegate) {
this.ext = ext;
this.delegate = delegate;
{
let [,xml,] = Gio.file_new_for_path(IFACE).load_contents(null);
xml = '' + ByteArray.toString(xml);
this.dbus_impl = Gio.DBusExportedObject.wrapJSObject(xml, this);
this.dbus_impl.export(Gio.DBus.session, '/timepp/zagortenay333/TimeTracker');
}
this.number_of_tracked_tasks = 0;
this.tic_mainloop_id = 0;
// Holds the path of the current csv directory.
// We also use this as a flag to check whether the tracker is active.
this.csv_dir = this.get_csv_dir_path();
this.yearly_csv_dir = null;
this.yearly_csv_file = null;
this.daily_csv_file = null;
this.yearly_csv_dir_monitor = null;
this.yearly_csv_file_monitor = null;
this.daily_csv_file_monitor = null;
this.yearly_csv_dir_monitor_id = null;
this.yearly_csv_file_monitor_id = null;
this.daily_csv_file_monitor_id = null;
// @stats_data: Map
// - @key: string (date in 'yyyy-mm-dd' iso format)
// - @val: array of objects of form:
// {
// label : string (a project or task)
// type : string ('++' or '()')
// total_time : int (seconds spent working on this task/proj)
// intervals : string
// }
//
// The keys in @stats_data are sorted from newest to oldest.
// In each @val inside @stats_data, the projects are sorted after tasks.
this.stats_data = new Map();
// @stats_unique_task : Set (of all unique tasks strings)
// @stats_unique_projcets : Set (of all unique projects strings)
this.stats_unique_tasks = new Set();
this.stats_unique_projects = new Set();
// string (in yyyy-mm-dd iso format)
// This is the oldest date in the stats data entry
this.oldest_date = '';
// @key: string
// - task string (a single line in the todo.txt file)
// - or project keyword (e.g., '+my_project', '+stuff', etc...)
//
// @val: obj
// of the form: {
// time : int (miscroseconds)
// start_time : int (microseconds for elapsed time computing)
// tracking : bool
// type : string ('++' = project, '()' = task)
// intervals : string
// }
//
// If @type is '()', then @val also has the prop:
// task_ref: obj (the ref of the corresponding task object)
//
// If @type is '++', then @val also has the prop:
// @tracked_children: int (number of tasks that are part of this
// project and that are being tracked)
this.daily_csv_map = new Map();
//
// listen
//
this.new_day_sig_id = this.delegate.connect('new-day', () => this._on_new_day_started());
this.ext.connect('start-time-tracking-by-id', (_1, _2, task_id) => {
this.start_tracking_by_id(task_id);
});
this.ext.connect('stop-time-tracking-by-id', (_1, _2, task_id) => {
this.stop_tracking_by_id(task_id);
});
//
// finally
//
this._init_finish();
}
_init_finish () {
this._init_tracker_dir();
this._init_daily_csv_map();
this._archive_yearly_csv_file();
}
_init_tracker_dir () {
this._disable_file_monitors();
if (! this.csv_dir) return;
let d = new Date();
try {
// yearly dir
this.yearly_csv_dir = MISC_UTILS.file_new_for_path(
`${this.csv_dir}/YEARS__time_tracker`);
if (! this.yearly_csv_dir.query_exists(null))
this.yearly_csv_dir.make_directory_with_parents(null);
// yearly file
this.yearly_csv_file = MISC_UTILS.file_new_for_path(
`${this.csv_dir}/${d.getFullYear()}__time_tracker.csv`);
if (! this.yearly_csv_file.query_exists(null))
this.yearly_csv_file.create(Gio.FileCreateFlags.NONE, null);
// daily file
this.daily_csv_file = MISC_UTILS.file_new_for_path(`${this.csv_dir}/TODAY__time_tracker.csv`);
if (! this.daily_csv_file.query_exists(null))
this.daily_csv_file.create(Gio.FileCreateFlags.NONE, null);
this._enable_file_monitors();
} catch (e) {
logError(e);
this.csv_dir = "";
}
}
_init_daily_csv_map () {
if (! this.csv_dir) return;
this.daily_csv_map.clear();
let today = MISC_UTILS.date_yyyymmdd();
let [, lines] = this.daily_csv_file.load_contents(null);
lines = ByteArray.toString(lines).split(/\r?\n/).filter((l) => /\S/.test(l));
let do_write_daily_csv_file = false;
let tasks_to_be_tracked = [];
for (let i = 0; i < lines.length; i++) {
let [e, date, time, type, val, intervals] = this._parse_csv_line(lines[i]);
if (e) {
let file_path = this.daily_csv_file.get_path();
let msg = 'line: %d\nfile: %s'.format(i, file_path);
Main.notify(_('Error while parsing csv file. See logs.'), msg);
log("ERROR timepp (csv file):\nline: %d\nfile: %s".format(i, file_path));
continue;
}
if (date !== today) {
this._archive_daily_csv_file(date.slice(0, 4));
this.daily_csv_map.clear();
return;
}
if (intervals.endsWith('..')) {
intervals = intervals.slice(0, -2);
if (type === '()') tasks_to_be_tracked.push(val);
do_write_daily_csv_file = true;
}
let entry = {
tracking : false,
type : type,
time : time * 1000000,
intervals : intervals,
};
let t = GLib.get_monotonic_time();
if (type === '++') entry.tracked_children = 0;
else entry.task_ref = null;
this.daily_csv_map.set(val, entry);
}
if (do_write_daily_csv_file) this._write_daily_csv_file();
if (this.delegate.settings.get_boolean('todo-resume-tracking')) {
for (let task_str of tasks_to_be_tracked) {
for (let task of this.delegate.tasks) {
if (task.task_str === task_str) {
this.start_tracking(task);
break;
}
}
}
}
}
_write_daily_csv_file () {
if (! this.csv_dir) return;
let today = MISC_UTILS.date_yyyymmdd();
let projects = '';
let tasks = '';
for (let [k, v] of this.daily_csv_map) {
let line = this._create_csv_line(today, v.time, v.type, k, v.intervals);
if (v.type === '++') projects += line;
else tasks += line;
}
this._disable_file_monitors();
try {
this.daily_csv_file.replace_contents(projects + tasks, null, false,
Gio.FileCreateFlags.REPLACE_DESTINATION, null);
} catch (e) { logError(e); }
this._enable_file_monitors();
}
// @year: string
_archive_daily_csv_file (year = null) {
if (! this.csv_dir) return;
this._disable_file_monitors();
try {
let [, contents] = this.daily_csv_file.load_contents(null);
let append_stream;
if (year) {
append_stream = MISC_UTILS.file_new_for_path(`${this.csv_dir}/${year}__time_tracker.csv`);
append_stream = append_stream.append_to(Gio.FileCreateFlags.NONE, null);
} else {
append_stream = this.yearly_csv_file.append_to(Gio.FileCreateFlags.NONE, null);
}
append_stream.write_all(contents, null);
this.daily_csv_file.replace_contents('', null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, null);
} catch (e) { logError(e); }
this._enable_file_monitors();
let d = new Date();
let time_str = "%02d:%02d:%02d".format(d.getHours(), d.getMinutes(), d.getSeconds());
let d_str = MISC_UTILS.date_yyyymmdd(d);
for (let [,v] of this.daily_csv_map) {
v.time = 0;
v.date = d_str;
if (v.tracking) v.intervals = `${time_str}..${time_str}..`;
else v.intervals = '';
}
}
// Returns bool (true = we archived the yearly file)
_archive_yearly_csv_file () {
if (! this.csv_dir) return;
let d = new Date();
let prev_f = `${this.csv_dir}/${d.getFullYear() - 1}__time_tracker.csv`;
if (GLib.file_test(prev_f, GLib.FileTest.EXISTS)) {
this._disable_file_monitors();
let dir = `${this.csv_dir}/YEARS__time_tracker`;
Util.spawnCommandLine(`mv ${prev_f} ${dir}`);
this._enable_file_monitors();
return true;
}
return false;
}
_enable_file_monitors (timeout = 100) {
if (!this.daily_csv_file_monitor) {
this.daily_csv_file_monitor = this.daily_csv_file.monitor(Gio.FileMonitorFlags.NONE, null);
}
if (!this.yearly_csv_file_monitor) {
this.yearly_csv_file_monitor = this.yearly_csv_file.monitor(Gio.FileMonitorFlags.NONE, null);
}
if (!this.yearly_csv_dir_monitor) {
this.yearly_csv_dir_monitor = this.yearly_csv_dir.monitor(Gio.FileMonitorFlags.NONE, null);
}
Mainloop.timeout_add(timeout, () => {
this.daily_csv_file_monitor_id = this.daily_csv_file_monitor.connect('changed', (...args) => {
this._on_tracker_files_modified();
});
this.yearly_csv_file_monitor_id = this.yearly_csv_file_monitor.connect('changed', (...args) => {
this._on_tracker_files_modified();
});
this.yearly_csv_dir_monitor_id = this.yearly_csv_dir_monitor.connect('changed', (...args) => {
this._on_tracker_files_modified();
});
});
}
_disable_file_monitors () {
if (this.daily_csv_file_monitor)
this.daily_csv_file_monitor.disconnect(this.daily_csv_file_monitor_id);
if (this.yearly_csv_file_monitor)
this.yearly_csv_file_monitor.disconnect(this.yearly_csv_file_monitor_id);
if (this.yearly_csv_dir_monitor)
this.yearly_csv_dir_monitor.disconnect(this.yearly_csv_dir_monitor);
}
_tracker_tic (...args) {
let d = new Date();
let time = GLib.get_monotonic_time();
let time_str = "%02d:%02d:%02d".format(d.getHours(), d.getMinutes(), d.getSeconds());
for (let [,v] of this.daily_csv_map) {
if (v.tracking) {
v.time = time - v.start_time;
v.intervals = v.intervals.slice(0, -10) + time_str + '..';
}
}
let seconds = args[0] || 30;
if (seconds === 30) {
seconds = 0;
this._write_daily_csv_file();
}
this.tic_mainloop_id = Mainloop.timeout_add_seconds(1, () => {
this._tracker_tic(seconds + 1);
});
}
_on_new_day_started () {
if (! this.csv_dir) this._init_finish();
this._archive_daily_csv_file();
let yearly_file_was_archived = this._archive_yearly_csv_file();
// To ensure a fresh yearly csv file.
if (yearly_file_was_archived) this._init_tracker_dir();
}
_on_tracker_files_modified () {
this.stop_all_tracking(false);
this.daily_csv_map.clear();
this.stats_data.clear();
this.stats_unique_tasks.clear();
this.stats_unique_projects.clear();
this._init_finish();
}
_parse_csv_line (line) {
let res = [false];
let field = '';
let inside_quotes = false;
for (let i = 0, len = line.length; i < len; i++) {
let ch = line[i];
if (ch === '"') {
if (i+1 === len || line[i+1] === ',') { // quote at end of field
inside_quotes = false;
res.push(field);
field = '';
i += 2; // eat the next comma and space after it
}
else if (!field) { // quote at start of field
inside_quotes = true;
}
else {
field += '"';
}
} else if (ch === ',' && !inside_quotes) {
res.push(field);
field = '';
i++; // eat space after comma
} else {
field += ch;
}
}
res.push(field);
if (!REG.ISO_DATE.test(res[1]) ||
!/\d{2}:\d{2}(:\d{2})?$/.test(res[2]) ||
(res[3] !== '++' && res[3] !== '()')) {
return [true];
}
// No intervals field found (backwards compatibility.)
if (res.length !== 6) res.push('');
let t = res[2].split(':');
res[2] = +(t[0])*3600 + +(t[1])*60 + (t.length === 3 ? +(t[2]) : 0);
res[4] = res[4].replace(/""/g, '"');
return res;
}
_create_csv_line (date, time, type, val, intervals) {
let h, m, s;
{
time = Math.round(time / 1000000);
h = Math.floor(time / 3600);
h = (h < 10) ? ('0' + h) : ('' + h);
time %= 3600;
m = Math.floor(time / 60);
m = (m < 10) ? ('0' + m) : ('' + m);
s = time % 60;
s = (s < 10) ? ('0' + s) : ('' + s);
}
val = val.replace(/"/g, '""');
if (intervals) {
let tokens = intervals.split('||');
let non_zero_intervals = [];
// Remove zero-length intervals.
for (let token of tokens) {
// We don't remove zero-length intervals that are open (i.e.,
// the last interval.)
if (token.endsWith('..')) {
non_zero_intervals.push(token);
continue;
}
let [start, end] = token.split('..');
if (start !== end) non_zero_intervals.push(token);
}
intervals = non_zero_intervals.join('||');
return `${date}, ${h}:${m}:${s}, ${type}, "${val}", ${intervals}\n`;
} else {
return `${date}, ${h}:${m}:${s}, ${type}, "${val}"\n`;
}
}
toggle_tracking (task) {
let val = this.daily_csv_map.get(task.task_str);
if (val && val.tracking) this.stop_tracking(task);
else this.start_tracking(task);
}
stop_all_tracking (do_write_daily_csv_file = true, close_intervals = true) {
if (! this.csv_dir) return;
for (let [k, v] of this.daily_csv_map) {
if (v.type === '()' && v.tracking)
this.stop_tracking(v.task_ref, false, close_intervals);
}
if (do_write_daily_csv_file) this._write_daily_csv_file();
}
start_tracking_by_id (id) {
if (! this.csv_dir) return;
for (let it of this.delegate.tasks) {
if (it.tracker_id === id) this.start_tracking(it);
}
}
stop_tracking_by_id (id) {
if (! this.csv_dir) return;
for (let it of this.delegate.tasks) {
if (it.tracker_id === id) this.stop_tracking(it);
}
}
start_tracking (task) {
if (! this.csv_dir) {
Main.notify(_('To track time, select a dir for csv files in the settings.'));
return;
}
if (task.completed) return;
let val = this.daily_csv_map.get(task.task_str);
if (val && val.tracking) return;
let start_time = GLib.get_monotonic_time();
let d = new Date();
let time_str = "%02d:%02d:%02d".format(d.getHours(), d.getMinutes(), d.getSeconds());
if (val) {
val.tracking = true;
val.task_ref = task;
val.start_time = start_time - val.time;
if (val.intervals) val.intervals += `||${time_str}..${time_str}..`;
else val.intervals = `${time_str}..${time_str}..`;
} else {
this.daily_csv_map.set(task.task_str, {
time : 0,
start_time : start_time,
tracking : true,
type : '()',
task_ref : task,
intervals : `${time_str}..${time_str}..`,
});
}
for (let project of task.projects) {
let val = this.daily_csv_map.get(project);
if (val) {
val.tracked_children++;
if (!val.tracking) {
val.tracking = true;
val.start_time = start_time - val.time;
if (val.intervals) val.intervals += `||${time_str}..${time_str}..`;
else val.intervals = `${time_str}..${time_str}..`;
}
} else {
this.daily_csv_map.set(project, {
time : 0,
tracking : true,
type : '++',
tracked_children : 1,
start_time : start_time,
intervals : `${time_str}..${time_str}..`,
});
}
}
this.number_of_tracked_tasks++;
if (this.tic_mainloop_id === 0) this._tracker_tic();
this.delegate.panel_item.actor.add_style_class_name('on');
for (let it of this.delegate.tasks) {
if (it.task_str === task.task_str) it.on_tracker_started();
}
this.dbus_impl.emit_signal('started_tracking', GLib.Variant.new('(s)', [task.task_str]));
}
stop_tracking (task, do_write_daily_csv_file = true, close_intervals = true) {
if (!this.csv_dir) return;
let val = this.daily_csv_map.get(task.task_str);
if (!val || !val.tracking) return;
val.tracking = false;
this.number_of_tracked_tasks--;
if (close_intervals) {
if (val.intervals.endsWith('..'))
val.intervals = val.intervals.slice(0, -2);
}
for (let project of task.projects) {
let val = this.daily_csv_map.get(project);
if (--val.tracked_children === 0) {
val.tracking = false;
if (close_intervals) {
if (val.intervals.endsWith('..'))
val.intervals = val.intervals.slice(0, -2);
}
}
}
for (let it of this.delegate.tasks) {
if (it.task_str === task.task_str) it.on_tracker_stopped();
}
if (this.number_of_tracked_tasks === 0) {
this.delegate.panel_item.actor.remove_style_class_name('on');
if (this.tic_mainloop_id > 0) {
Mainloop.source_remove(this.tic_mainloop_id);
this.tic_mainloop_id = 0;
}
}
if (do_write_daily_csv_file) this._write_daily_csv_file();
this.dbus_impl.emit_signal('stopped_tracking', GLib.Variant.new('(s)', [task.task_str]));
}
get_tracked_tasks () {
let res = "";
for (let [k, v] of this.daily_csv_map) {
if (v.tracking && v.type === '()') res += k + "___timepp___";
}
return res ? res : "none";
}
get_tracked_projects () {
let res = "";
for (let [k, v] of this.daily_csv_map) {
if (v.tracking && v.type === '++') res += k + "___timepp___";
}
return res ? res : "none";
}
// Swap the old_task_str with the new_task_str in the daily_csv_map only.
// The time tracked on the old_task_str is copied over to the new_task_str.
update_record_name (old_task_str, new_task_str) {
if (!this.csv_dir || this.daily_csv_map.get(new_task_str)) return;
let val = this.daily_csv_map.get(old_task_str);
if (! val) return;
this.daily_csv_map.delete(old_task_str);
this.daily_csv_map.set(new_task_str, val);
// We would like to delete the old task from the stats entries, but we
// can't tell whether or not we tracked the old task on days prior to
// today.
// We clear the cached stats data to let get_stats() rebuild it.
this.stats_unique_tasks.clear();
this.stats_unique_projects.clear();
this.stats_data.clear();
this._write_daily_csv_file();
}
get_csv_dir_path () {
let d = this.delegate.get_current_todo_file();
if (!d) return "";
return d.time_tracker_dir;
}
// NOTE: The returned values are cached, use for READ-ONLY!
//
// returns: [@stats_data, @stats_unique_tasks, @stats_unique_projects, oldest_date]
get_stats () {
if (! this.csv_dir) return null;
let today = MISC_UTILS.date_yyyymmdd();
let oldest_date = this.oldest_date ? this.oldest_date : today;
let stats_unique_tasks = this.stats_unique_tasks;
let stats_unique_projects = this.stats_unique_projects;
// update todays data
{
let stats_today = [];
for (let [k, v] of this.daily_csv_map) {
if (v.type === '()') stats_unique_tasks.add(k);
else stats_unique_projects.add(k);
let record = {
label : k,
type : v.type,
total_time : Math.round(v.time / 1000000),
intervals : v.intervals,
};
if (v.type === '++') stats_today.push(record);
else stats_today.unshift(record);
}
this.stats_data.set(today, stats_today);
}
// add the rest if we don't have it cached
if (this.stats_data.size < 2) {
let reg = /^\d{4}__time_tracker.csv$/;
let csv_files = [];
let file_enum;
try {
file_enum = this.yearly_csv_dir.enumerate_children(
'standard::name,standard::type',
Gio.FileQueryInfoFlags.NONE,
null
);
} catch (e) { file_enum = null; }
if (file_enum !== null) {
let info;
while ((info = file_enum.next_file(null))) {
if (! reg.test(info.get_name())) continue;
csv_files.push([file_enum.get_child(info), info.get_name()]);
}
}
csv_files.push([this.yearly_csv_file, this.yearly_csv_file.get_basename()]);
csv_files.sort((a, b) => a[1] < b[1]);
for (let file of csv_files) {
let [, content] = file[0].load_contents(null);
content = String(content).split(/[\r\n]/).filter((l) => /\S/.test(l));
let parse = this._parse_csv_line;
let stats_data = this.stats_data;
let found_at_least_one_error = false;
let i = content.length;
while (i--) {
let [e, date, time, type, label, intervals] = parse(content[i]);
if (e) {
let file_path = file[0].get_path();
log("ERROR timepp (csv file):\nline: %d\nfile: %s".format(i+1, file_path));
// If there are multiple errors, we don't want to bring
// the system to a halt we a bazillion notifs.
if (!found_at_least_one_error) {
let msg = 'line: %d\nfile: %s'.format(i+1, file_path);
Main.notify(_('Error while parsing csv file. See logs.'), msg);
found_at_least_one_error = true;
}
continue;
}
if (type === '()') stats_unique_tasks.add(label);
else stats_unique_projects.add(label);
let record = {
label : label,
type : type,
total_time : time,
intervals : intervals,
};
let records = stats_data.get(date);
if (records) records.push(record);
else stats_data.set(date, [record]);
oldest_date = date;
}
}
}
this.oldest_date = oldest_date;
return [this.stats_data, this.stats_unique_tasks, this.stats_unique_projects, oldest_date];
}
close () {
this.stop_all_tracking(true, false);
if (this.daily_csv_file_monitor) {
this.daily_csv_file_monitor.cancel();
this.daily_csv_file_monitor = null;
}
if (this.yearly_csv_file_monitor) {
this.yearly_csv_file_monitor.cancel();
this.yearly_csv_file_monitor = null;
}
if (this.yearly_csv_dir_monitor) {
this.yearly_csv_dir_monitor.cancel();
this.yearly_csv_dir_monitor = null;
}
if (this.new_day_sig_id) this.delegate.disconnect(this.new_day_sig_id);
if (this.todo_current_sig_id) this.delegate.settings.disconnect(this.todo_current_sig_id);
this.daily_csv_map.clear();
this.stats_data.clear();
this.stats_unique_tasks.clear();
this.stats_unique_projects.clear();
this.dbus_impl.unexport();
}
}
Signals.addSignalMethods(TimeTracker.prototype);

View File

@@ -0,0 +1,117 @@
const St = imports.gi.St;
const Clutter = imports.gi.Clutter;
const Main = imports.ui.main;
const Signals = imports.signals;
const Mainloop = imports.mainloop;
const ME = imports.misc.extensionUtils.getCurrentExtension();
const MISC_UTILS = ME.imports.lib.misc_utils;
const Gettext = imports.gettext.domain(ME.metadata['gettext-domain']);
const _ = Gettext.gettext;
const ngettext = Gettext.ngettext;
const G = ME.imports.sections.todo.GLOBAL;
// =====================================================================
// @@@ ViewClearTasks
//
// @ext : obj (main extension object)
// @delegate : obj (main section object)
//
// @signals:
// - 'delete-all' (delete all completed tasks)
// - 'archive-all' (delete and write to done.txt all completed tasks)
// - 'cancel'
// =====================================================================
var ViewClearTasks = class ViewClearTasks {
constructor (ext, delegate) {
this.ext = ext;
this.delegate = delegate;
//
// container
//
this.actor = new St.Bin({ x_fill: true, style_class: 'view-clear-tasks view-box' });
this.content_box = new St.BoxLayout({ x_expand: true, vertical: true, style_class: 'view-box-content' });
this.actor.add_actor(this.content_box);
//
// options
//
{
this.delete_all_item = new St.BoxLayout({ reactive: true, style_class: 'row delete-completed-tasks' });
this.content_box.add_child(this.delete_all_item);
this.delete_all_item.add(new St.Icon ({ gicon : MISC_UTILS.getIcon('timepp-radioactive-symbolic') }));
this.delete_all_item.add(new St.Label ({ text: _('Delete all completed tasks'), x_expand: true, y_align: Clutter.ActorAlign.CENTER }));
this.delete_all_radiobutton = new St.Button({ style_class: 'radiobutton', toggle_mode: true, can_focus: true, y_align: St.Align.MIDDLE });
this.delete_all_item.add_child(this.delete_all_radiobutton);
let delete_all_checkmark = new St.Bin();
this.delete_all_radiobutton.add_actor(delete_all_checkmark);
}
{
this.archive_all_item = new St.BoxLayout({ reactive: true, style_class: 'row rchive-all-completed-tasks-label' });
this.content_box.add_child(this.archive_all_item);
this.archive_all_item.add(new St.Label ({ text: _('Archive all completed tasks to done.txt and delete them'), x_expand: true, y_align: Clutter.ActorAlign.CENTER }));
this.archive_all_radiobutton = new St.Button({ style_class: 'radiobutton', toggle_mode: true, can_focus: true, y_align: St.Align.MIDDLE });
this.archive_all_item.add_child(this.archive_all_radiobutton);
let archive_all_checkmark = new St.Bin();
this.archive_all_radiobutton.add_actor(archive_all_checkmark);
let current = this.delegate.get_current_todo_file();
if (current && current.done_file) {
this.archive_all_radiobutton.checked = true;
} else {
this.archive_all_item.hide();
this.delete_all_radiobutton.checked = true;
}
}
//
// buttons
//
this.btn_box = new St.BoxLayout({ x_expand: true, style_class: 'row btn-box' });
this.content_box.add_child(this.btn_box);
this.button_delete = new St.Button({ can_focus: true, label: _('Delete'), style_class: 'btn-delete button', x_expand: true });
this.btn_box.add(this.button_delete, {expand: true});
this.button_cancel = new St.Button({ can_focus: true, label: _('Cancel'), style_class: 'btn-cancel button notification-icon-button modal-dialog-button' });
this.btn_box.add(this.button_cancel, {expand: true});
//
// listen
//
this.archive_all_radiobutton.connect('clicked', () => { this.delete_all_radiobutton.checked = false; });
this.delete_all_radiobutton.connect('clicked', () => { this.archive_all_radiobutton.checked = false; });
this.button_cancel.connect('clicked', () => { this.emit('cancel'); });
this.button_delete.connect('clicked', () => {
if (this.delete_all_radiobutton.checked)
this.emit('delete-all');
else
this.emit('archive-all');
});
}
close () {
this.actor.destroy();
}
}
Signals.addSignalMethods(ViewClearTasks.prototype);

View File

@@ -0,0 +1,959 @@
const St = imports.gi.St;
const Gtk = imports.gi.Gtk;
const Shell = imports.gi.Shell;
const Clutter = imports.gi.Clutter;
const Main = imports.ui.main;
const Signals = imports.signals;
const Mainloop = imports.mainloop;
const ME = imports.misc.extensionUtils.getCurrentExtension();
const Gettext = imports.gettext.domain(ME.metadata['gettext-domain']);
const _ = Gettext.gettext;
const ngettext = Gettext.ngettext;
const SIG_MANAGER = ME.imports.lib.signal_manager;
const MISC_UTILS = ME.imports.lib.misc_utils;
const FUZZ = ME.imports.lib.fuzzy_search;
const REG = ME.imports.lib.regex;
const DND = ME.imports.lib.dnd;
const G = ME.imports.sections.todo.GLOBAL;
// =====================================================================
// @@@ ViewDefault
//
// @ext : obj (main extension object)
// @delegate : obj (main section object)
// =====================================================================
var ViewDefault = class ViewDefault {
constructor (ext, delegate) {
this.ext = ext;
this.delegate = delegate;
this.kanban_string = "";
this.kanban_columns = new Map();
this.task_with_active_kanban_str = null;
this.add_tasks_to_menu_mainloop_id = null;
this.tasks_viewport = [];
this.needs_filtering = true;
this.automatic_sort = this.delegate.get_current_todo_file().automatic_sort;
this.has_active_filters = this._has_active_filters();
this.tasks_added_to_menu = false;
this.sigm = new SIG_MANAGER.SignalManager();
//
// draw
//
this.actor = new St.BoxLayout({ vertical: true, x_expand: true, style_class: 'view-default view-box' });
this.dummy_focus_actor = new St.Widget({ visible: false, width: 0, height: 0 });
this.actor.add_child(this.dummy_focus_actor);
this.columns_scroll = new St.ScrollView({ vscrollbar_policy: Gtk.PolicyType.NEVER,});
this.actor.add_actor(this.columns_scroll);
this.content_box = new St.BoxLayout({ x_align: Clutter.ActorAlign.CENTER, x_expand: true, y_expand: true, style_class: 'view-box-content' });
this.columns_scroll.add_actor(this.content_box);
//
// listen
//
this.sigm.connect(this.delegate, 'section-open-state-changed', (_, state) => {
if (!this.tasks_added_to_menu) this._add_tasks_to_menu();
});
this.columns_scroll.connect('scroll-event', (_, event) => this.horiz_scroll(event));
this.content_box.connect('allocation-changed', () => {
this.columns_scroll.hscrollbar_policy = Gtk.PolicyType.NEVER;
let [, nat_w] = this.content_box.get_preferred_width(-1);
let max_w = this.ext.menu_max_w;
if (nat_w >= max_w) this.columns_scroll.hscrollbar_policy = Gtk.PolicyType.ALWAYS;
});
this.sigm.connect(this.actor, 'event', (_, event) => {
switch (event.get_key_symbol()) {
case Clutter.KEY_slash:
this.delegate.show_view__search();
break;
case Clutter.KEY_f:
this.delegate.show_view__file_switcher();
break;
case Clutter.KEY_i:
this.delegate.show_view__task_editor();
break;
case Clutter.KEY_k:
this.delegate.show_view__kanban_switcher();
break;
case Clutter.KEY_y:
this.delegate.show_view__filters();
break;
case Clutter.KEY_s:
this.delegate.show_view__sort();
break;
}
});
//
// finally
//
this._init_columns();
}
_init_columns () {
let w = this.delegate.settings.get_int('todo-task-width') + 20;
this._clear_kanban_columns();
let [success, task, str] = this._get_active_kanban_board();
if (success) {
this.kanban_string = str;
this.task_with_active_kanban_str = task;
let columns = str.slice(str.indexOf('|')+1).split('|');
for (let it of columns) {
let is_collapsed = (it[0] === '_');
if (is_collapsed) it = it.slice(1);
let column = new KanbanColumn(this.ext, this.delegate, this, it, is_collapsed);
column.tasks_scroll_content.style = `width: ${w}px;`;
this.kanban_columns.set(it, column);
this.content_box.add_child(column.actor);
}
} else {
let column = new KanbanColumn(this.ext, this.delegate, this, '$', false);
column.tasks_scroll_content.style = `width: ${w}px;`;
this.kanban_columns.set('$', column);
this.content_box.add_child(column.actor);
}
if (this.kanban_columns.size === 1) this.delegate.actor.add_style_class_name('one-column');
if (this.ext.menu.isOpen) this._add_tasks_to_menu();
}
_get_active_kanban_board () {
for (let it of this.delegate.tasks) {
if (! it.kanban_boards) continue;
for (let str of it.kanban_boards) {
if (str[4] === '*') return [true, it, str];
}
}
return [false, null, null];
}
_clear_kanban_columns () {
this._remove_tasks_from_menu();
for (let [,column] of this.kanban_columns) column.close();
this.kanban_columns.clear();
}
_add_tasks_to_menu () {
if (this.add_tasks_to_menu_mainloop_id) {
Mainloop.source_remove(this.add_tasks_to_menu_mainloop_id);
this.add_tasks_to_menu_mainloop_id = null;
}
this.tasks_added_to_menu = true;
for (let [,column] of this.kanban_columns)
column.tasks_scroll_content.remove_all_children();
let arr;
if (this.needs_filtering) {
this.tasks_viewport = [];
arr = this.delegate.tasks;
} else {
arr = this.tasks_viewport;
}
this._add_tasks_to_menu__finish(0, arr, false);
}
_add_tasks_to_menu__finish (i, arr, scrollbar_shown) {
let n = 50;
for (let j = 0; j < n; j++, i++) {
if (i === arr.length) {
this.needs_filtering = false;
this.add_tasks_to_menu_mainloop_id = null;
for (let [,col] of this.kanban_columns) col.set_title();
Mainloop.idle_add(() => this.update_scrollbars());
return;
}
let it = arr[i];
if (it.actor_parent) continue;
let column = this._get_column(it);
if (! column) continue;
if (this.needs_filtering) {
if (!this._filter_test(it)) {
n++;
continue;
}
this.tasks_viewport.push(it);
}
column.tasks_scroll_content.add_child(it.actor);
it.owner = column;
it.actor_parent = column.tasks_scroll_content;
it.actor_scrollview = [[column.tasks_scroll], [this.columns_scroll]];
it.dnd.drag_enabled = true;
}
this.update_scrollbars();
this.add_tasks_to_menu_mainloop_id = Mainloop.idle_add(() => {
this._add_tasks_to_menu__finish(i, arr, scrollbar_shown);
});
}
_remove_tasks_from_menu () {
if (this.add_tasks_to_menu_mainloop_id) {
Mainloop.source_remove(this.add_tasks_to_menu_mainloop_id);
this.add_tasks_to_menu_mainloop_id = null;
}
for (let [,column] of this.kanban_columns) {
column.tasks_scroll_content.remove_all_children();
}
for (let task of this.tasks_viewport) {
task.actor_parent = null;
task.actor_scrollview = null;
task.owner = null;
}
}
// If we only have one column, then we hide it's vertical scrollbar if it's
// not needed since the extra space that gets allocated for it is ugly.
//
// With multiple columns, we get all sorts of little issues with respect to
// dnd that require nasty hacks.
// It's not worth it, so we set the scrollbar to AUTOMATIC.
update_scrollbars () {
if (!this.ext.menu.isOpen || this.kanban_columns.size > 1) return;
for (let [,col] of this.kanban_columns) {
col.tasks_scroll.vscrollbar_policy = Gtk.PolicyType.NEVER;
if (this.ext.needs_scrollbar()) col.tasks_scroll.vscrollbar_policy = Gtk.PolicyType.ALWAYS;
}
}
// @task: obj (a task object)
//
// A predicate used to determine whether a task inside the this.tasks array
// will be added to this.tasks_viewport array (i.e., whether it can be
// visible to the user).
//
// If invert_filters is false, return true if at least one filter is matched.
// If invert_filters is true, return false if at least one filter is matched.
_filter_test (task) {
let filters = this.delegate.get_current_todo_file().filters;
if (task.pinned) return true;
if (filters.hidden) return task.hidden;
if (task.hidden) return false;
if (filters.deferred) return task.is_deferred;
if (filters.recurring) return Boolean(task.rec_str);
if (task.rec_str && task.completed) return false;
if (task.is_deferred) return false;
if (! this.has_active_filters) return true;
if (task.completed) {
if (filters.completed)
return !filters.invert_filters;
}
else if (task.priority === '(_)') {
if (filters.no_priority)
return !filters.invert_filters;
}
for (let it of filters.priorities) {
if (it === task.priority)
return !filters.invert_filters;
}
for (let it of filters.contexts) {
if (task.contexts.indexOf(it) !== -1)
return !filters.invert_filters;
}
for (let it of filters.projects) {
if (task.projects.indexOf(it) !== -1)
return !filters.invert_filters;
}
for (let it of filters.custom_active) {
if (FUZZ.fuzzy_search_v1(it, task.task_str) !== null)
return !filters.invert_filters;
}
return filters.invert_filters;
}
_has_active_filters () {
let filters = this.delegate.get_current_todo_file().filters;
if (filters.deferred ||
filters.recurring ||
filters.hidden ||
filters.completed ||
filters.no_priority ||
filters.priorities.length ||
filters.contexts.length ||
filters.projects.length ||
filters.custom_active.length) {
return true;
}
return false;
}
_toggle_filters () {
let filters = this.delegate.get_current_todo_file().filters;
filters.invert_filters = !filters.invert_filters;
this.filter_icon.gicon = MISC_UTILS.getIcon(
filters.invert_filters ?
'timepp-filter-inverted-symbolic' :
'timepp-filter-symbolic'
)
this.needs_filtering = true;
this._add_tasks_to_menu();
this.delegate.store_cache();
}
toggle_automatic_sort () {
let state = !this.automatic_sort;
if (state) {
for (let [,col] of this.kanban_columns)
col.sort_icon.add_style_class_name('active');
} else {
for (let [,col] of this.kanban_columns)
col.sort_icon.remove_style_class_name('active');
}
this.delegate.get_current_todo_file().automatic_sort = state;
this.automatic_sort = state;
this.delegate.store_cache();
if (state) this.delegate.on_tasks_changed(true, true);
}
on_drag_end (old_parent, new_parent, column) {
this.update_kan_string(false);
this.delegate.on_tasks_changed(true, true);
}
update_kan_string (write_to_file = true) {
let new_kanban_str = this.kanban_string.slice(0, this.kanban_string.indexOf('|'));
for (let it of this.content_box.get_children()) {
let col = it._owner;
if (col.is_collapsed) new_kanban_str += '|_' + col.col_str;
else new_kanban_str += '|' + col.col_str;
}
let t = this.task_with_active_kanban_str;
t.reset(true, t.task_str.replace(this.kanban_string, new_kanban_str));
this.kanban_string = new_kanban_str;
if (write_to_file) this.delegate.write_tasks_to_file();
}
_get_column (task) {
for (let [name, column] of this.kanban_columns) {
if (column.is_kitchen_sink) return column;
for (let it of column.filters) {
if (task.projects.indexOf(it) !== -1 || task.contexts.indexOf(it) !== -1 || task.priority === it) {
return column;
}
}
}
return null;
}
horiz_scroll (event) {
let direction = event.get_scroll_direction();
let delta = 0;
if (direction === Clutter.ScrollDirection.UP) delta = -1;
else if (direction === Clutter.ScrollDirection.DOWN) delta = 1;
else return Clutter.EVENT_PROPAGATE;
let bar = this.columns_scroll.get_hscroll_bar();
if (! bar) return;
let a = bar.get_adjustment();
a.value += delta * a.stepIncrement;
}
close () {
this.sigm.clear();
this._clear_kanban_columns();
this.actor.destroy();
this.actor = null;
}
}
Signals.addSignalMethods(ViewDefault.prototype);
// =====================================================================
// @@@ KanbanColumn
//
// @ext : obj (main extension object)
// @delegate : obj (main section object)
// @owner : obj (@@@ ViewDefault)
// @col_str : string
// @is_collapsed : bool
// =====================================================================
var KanbanColumn = class KanbanColumn {
constructor (ext, delegate, owner, col_str, is_collapsed) {
this.ext = ext;
this.delegate = delegate;
this.owner = owner;
this.col_str = col_str;
this.is_collapsed = is_collapsed;
this.actor_scrollview = [[], [this.owner.columns_scroll]];
this.actor_parent = this.owner.content_box;
this.filters = this.col_str.split(',');
this.is_kitchen_sink = true;
this.title_visible = false;
for (let it of this.filters) {
if (it && it !== '$') this.title_visible = true;
if (REG.TODO_CONTEXT.test(it) || REG.TODO_PROJ.test(it) || it === '(_)' || REG.TODO_PRIO.test(it)) {
this.is_kitchen_sink = false;
break;
}
}
this.sigm = new SIG_MANAGER.SignalManager();
this.rotate_id = null;
//
// draw
//
this.actor = new St.BoxLayout({ vertical: true, y_expand: true, x_expand: false, style_class: 'kanban-column' });
this.actor._owner = this; // can't use _delegate here due to dnd !!!
this.content_box = new St.BoxLayout({ vertical: true, y_expand: true });
this.actor.add_child(this.content_box);
this.content_box._delegate = this;
//
// header
//
this.header_wrapper = new St.Widget({ style_class: 'timepp-menu-item header', layout_manager: new Clutter.BoxLayout(), });
this.content_box.add_child(this.header_wrapper);
this.header = new St.BoxLayout({ reactive: true, x_expand: true });
this.header_wrapper.add_child(this.header);
//
// kanban column title
//
this.kanban_title = new St.Label({ visible: this.title_visible, reactive: true, can_focus: true, x_expand: true, y_align: Clutter.ActorAlign.CENTER });
this.header.add_child(this.kanban_title);
//
// the functional part of the header (add task, ...)
//
this.header_fn_btns = new St.BoxLayout({ visible: !this.title_visible, x_expand: true });
this.header.add_child(this.header_fn_btns);
this.add_task_button = new St.Button({ can_focus: true, x_align: St.Align.START, style_class: 'add-task' });
this.header_fn_btns.add(this.add_task_button, { expand: true });
this.add_task_bin = new St.BoxLayout();
this.add_task_button.add_actor(this.add_task_bin);
this.add_task_icon = new St.Icon({ gicon : MISC_UTILS.getIcon('timepp-plus-symbolic'), y_align: Clutter.ActorAlign.CENTER });
this.add_task_bin.add_actor(this.add_task_icon);
this.add_task_label = new St.Label({ text: _('Add New Task...'), y_align: Clutter.ActorAlign.CENTER });
this.add_task_bin.add_actor(this.add_task_label);
//
// header icons
//
this.icon_box = new St.BoxLayout({ x_align: Clutter.ActorAlign.END, style_class: 'icon-box' });
this.header_fn_btns.add_child(this.icon_box);
this.collapse_icon = new St.Icon({ gicon : MISC_UTILS.getIcon('timepp-column-collapse-symbolic'), can_focus: true, reactive: true, track_hover: true, y_align: Clutter.ActorAlign.CENTER, style_class: 'collapse-icon' });
this.icon_box.add_child(this.collapse_icon);
this.kanban_icon = new St.Icon({ gicon : MISC_UTILS.getIcon('timepp-kanban-symbolic'), can_focus: true, reactive: true, track_hover: true, y_align: Clutter.ActorAlign.CENTER, style_class: 'kanban-icon' });
this.icon_box.add_child(this.kanban_icon);
this.clear_icon = new St.Icon({ gicon : MISC_UTILS.getIcon('timepp-clear-symbolic'), can_focus: true, reactive: true, track_hover: true, y_align: Clutter.ActorAlign.CENTER, style_class: 'clear-icon' });
this.icon_box.add_child(this.clear_icon);
this.clear_icon.visible = this.delegate.stats.completed > 0;
this.filter_icon = new St.Icon({ can_focus: true, reactive: true, track_hover: true, y_align: Clutter.ActorAlign.CENTER, style_class: 'filter-icon' });
this.icon_box.add_child(this.filter_icon);
if (this.owner.has_active_filters) this.filter_icon.add_style_class_name('active');
else this.filter_icon.remove_style_class_name('active');
if (this.delegate.get_current_todo_file().filters.invert_filters)
this.filter_icon.gicon = MISC_UTILS.getIcon('timepp-filter-inverted-symbolic');
else
this.filter_icon.gicon = MISC_UTILS.getIcon('timepp-filter-symbolic');
this.sort_icon = new St.Icon({ gicon : MISC_UTILS.getIcon('timepp-sort-ascending-symbolic'), can_focus: true, reactive: true, track_hover: true, y_align: Clutter.ActorAlign.CENTER, style_class: 'sort-icon' });
this.icon_box.add_child(this.sort_icon);
if (this.owner.automatic_sort) this.sort_icon.add_style_class_name('active');
else this.sort_icon.remove_style_class_name('active');
this.search_icon = new St.Icon({ gicon : MISC_UTILS.getIcon('timepp-search-symbolic'), can_focus: true, reactive: true, track_hover: true, y_align: Clutter.ActorAlign.CENTER, style_class: 'search-icon' });
this.icon_box.add_child(this.search_icon);
this.file_switcher_icon = new St.Icon({ gicon : MISC_UTILS.getIcon('timepp-file-symbolic'), can_focus: true, reactive: true, track_hover: true, y_align: Clutter.ActorAlign.CENTER, style_class: 'file-switcher-icon' });
this.icon_box.add_child(this.file_switcher_icon);
this.stats_icon = new St.Icon({ gicon : MISC_UTILS.getIcon('timepp-graph-symbolic'), can_focus: true, reactive: true, track_hover: true, y_align: Clutter.ActorAlign.CENTER, style_class: 'stats-icon' });
this.icon_box.add_child(this.stats_icon);
//
// task items box
//
this.tasks_scroll = new St.ScrollView({ hscrollbar_policy: Gtk.PolicyType.NEVER, style_class: 'timepp-menu-item tasks-container', y_align: St.Align.START});
this.content_box.add_child(this.tasks_scroll);
if (this.owner.kanban_columns.size > 1) this.tasks_scroll.vscrollbar_policy = Gtk.PolicyType.AUTOMATIC;
this.tasks_scroll_content = new St.BoxLayout({ vertical: true, style_class: 'tasks-content-box'});
this.tasks_scroll.add_actor(this.tasks_scroll_content);
//
// DND
//
// @HACK
// The task items are draggable and their container is also draggable.
// Gnome's dnd module propagates button-press-event and touch-event to
// the container when a task item is dragged which will result in
// dragging the container at the same time the task item is dragged.
// To prevent this, we also connect on those events and only react if
// the source was an actor that we whitelist (e.g., the tasks_scroll.)
//
// NOTE: We must connect on these before instantiating our dnd module.
this.actor.connect('button-press-event', (_, event) => this._on_maybe_drag(event));
this.actor.connect('touch-event', (_, event) => this._on_maybe_drag(event));
this.dnd = new DND.Draggable(this, G.DNDGroup.KANBAN_COLUMN, false);
//
// listen
//
this.sigm.connect(this.delegate.settings, 'changed::todo-task-width', () => {
let width = this.delegate.settings.get_int('todo-task-width');
this.tasks_scroll_content.style = `width: ${width}px;`;
});
this.sigm.connect(this.delegate.settings, 'changed::todo-task-width', () => {
this.header.set_width(this.delegate.settings.get_int('todo-task-width'));
});
this.sigm.connect(this.tasks_scroll, 'scroll-event', (_, event) => {
if (event.get_state() & Clutter.ModifierType.CONTROL_MASK) {
this.owner.horiz_scroll(event);
return Clutter.EVENT_STOP;
}
});
this.sigm.connect(this.header_fn_btns, 'event', (_, event) => {
if (! this.title_visible) return;
Mainloop.timeout_add(0, () => {
if (!this.header_fn_btns.contains(global.stage.get_key_focus())) {
this.kanban_title.show();
this.header_fn_btns.hide();
}
});
});
this.sigm.connect(this.kanban_title, 'key-focus-in', () => {
Mainloop.idle_add(() => {
if (global.stage.get_key_focus() === this.kanban_title) this._hide_title();
});
});
this.sigm.connect(this.header, 'leave-event', (_, event) => this._maybe_show_title(event));
this.sigm.connect(this.header, 'enter-event', () => this._hide_title());
this.sigm.connect(this.ext, 'custom-css-changed', () => this.set_title());
this.sigm.connect_release(this.add_task_button, Clutter.BUTTON_PRIMARY, true, () => this.delegate.show_view__task_editor());
this.sigm.connect_release(this.collapse_icon, Clutter.BUTTON_PRIMARY, true, () => this.toggle_collapse());
this.sigm.connect_release(this.kanban_icon, Clutter.BUTTON_PRIMARY, true, () => this.delegate.show_view__kanban_switcher());
this.sigm.connect_release(this.filter_icon, Clutter.BUTTON_PRIMARY, true, () => this.delegate.show_view__filters());
this.sigm.connect_on_button(this.filter_icon, Clutter.BUTTON_MIDDLE, () => this._toggle_filters());
this.sigm.connect_release(this.file_switcher_icon, Clutter.BUTTON_PRIMARY, true, () => this.delegate.show_view__file_switcher());
this.sigm.connect_release(this.search_icon, Clutter.BUTTON_PRIMARY, true, () => this.delegate.show_view__search());
this.sigm.connect_release(this.stats_icon, Clutter.BUTTON_PRIMARY, true, () => this.delegate.show_view__time_tracker_stats());
this.sigm.connect_release(this.clear_icon, Clutter.BUTTON_PRIMARY, true, () => this.delegate.show_view__clear_completed());
this.sigm.connect_release(this.sort_icon, Clutter.BUTTON_PRIMARY, true, () => this.delegate.show_view__sort());
this.sigm.connect_on_button(this.sort_icon, Clutter.BUTTON_MIDDLE, () => this.owner.toggle_automatic_sort());
//
// finally
//
if (is_collapsed) {
this.is_collapsed = false;
this.collapse(false);
}
}
toggle_collapse () {
if (this.is_collapsed) this.uncollapse();
else this.collapse();
}
collapse (update_kan_string = true) {
if (this.is_collapsed) return;
this.is_collapsed = true;
this.tasks_scroll.hide();
this.collapse_icon.gicon = MISC_UTILS.getIcon('timepp-column-uncollapse-symbolic');
this.actor.add_style_class_name('collapsed');
this.icon_box.remove_child(this.collapse_icon);
this.header.insert_child_at_index(this.collapse_icon, 0);
// Don't like the way we rotate the text, but it's the only thing I
// could come up with..
// We switch the layout manager of the header_wrapper in addition to
// rotating the header.
this.header.set_pivot_point(0, 1);
this.header.rotation_angle_z = 90;
this.header_wrapper.set_layout_manager(new Clutter.FixedLayout());
this.rotate_id = this.header.connect('allocation-changed', () => {
this.header_wrapper.set_width(this.header.get_height());
this.header.set_width(Math.floor(this.actor.get_height() * 0.9));
this.header.disconnect(this.rotate_id);
this.rotate_id = null;
});
if (update_kan_string) this.owner.update_kan_string();
}
uncollapse () {
if (! this.is_collapsed) return;
this.is_collapsed = false;
if (this.rotate_id) {
this.header.disconnect(this.rotate_id);
this.rotate_id = null;
}
this.tasks_scroll.show();
this.actor.set_height(-1);
this.header.set_width(-1);
this.header_wrapper.set_width(-1);
this.collapse_icon.gicon = MISC_UTILS.getIcon('timepp-column-collapse-symbolic');
this.actor.remove_style_class_name('collapsed');
this.header.remove_child(this.collapse_icon);
this.icon_box.insert_child_at_index(this.collapse_icon, 0);
this.header_wrapper.set_layout_manager(new Clutter.BoxLayout());
this.header.rotation_angle_z = 0;
this.owner.update_kan_string();
}
set_title () {
let markup = `<b>${this.tasks_scroll_content.get_n_children()}</b> `;
for (let i = 0; i < this.filters.length; i++) {
let it = this.filters[i];
if (REG.TODO_CONTEXT.test(it)) {
let c = this.ext.custom_css['-timepp-context-color'][0];
markup += `<span foreground="${c}"><b>${it}</b></span> `;
}
else if (REG.TODO_PROJ.test(it)) {
let c = this.ext.custom_css['-timepp-project-color'][0];
markup += `<span foreground="${c}"><b>${it}</b></span> `;
}
else {
markup += `<b>${it}</b> `;
}
}
this.kanban_title.clutter_text.set_markup(markup.replace(/\\ /g, ' '));
}
_maybe_show_title (event) {
if (! this.title_visible) return;
let related = event.get_related();
if (related && !this.header_fn_btns.contains(related)) {
this.header_fn_btns.hide();
this.kanban_title.show();
this.delegate.panel_item.actor.grab_key_focus();
}
}
_hide_title () {
if (this.is_collapsed) return;
this.kanban_title.hide();
this.header_fn_btns.show();
if (this.title_visible) this.header_fn_btns.get_first_child().grab_key_focus();
}
_on_maybe_drag (event) {
if (this.owner.kanban_columns.size < 2) return Clutter.EVENT_STOP;
switch (event.get_source()) {
case this.actor:
case this.header:
case this.tasks_scroll:
return Clutter.EVENT_PROPAGATE;
default:
return Clutter.EVENT_STOP;
}
}
// A task got dropped
on_drag_end (old_parent, new_parent, task) {
task.hide_header_icons();
if (old_parent === new_parent) {
if (this.delegate.get_current_todo_file().automatic_sort) {
this._sort_task_in_column(new_parent, task);
} else {
this._sort_task_in_arrays(task);
this.delegate.write_tasks_to_file();
}
return;
}
let [old_col, target_col, destination_column] = this._update_task_props(old_parent, new_parent, task);
if (target_col !== destination_column) {
task.actor_parent.remove_child(task.actor);
destination_column.tasks_scroll_content.add_child(task.actor);
task.actor_parent = destination_column.tasks_scroll_content;
}
task.owner = destination_column;
task.actor_parent = destination_column.tasks_scroll_content;
task.actor_scrollview = [[destination_column.tasks_scroll], [this.owner.columns_scroll]];
this.delegate.on_tasks_changed();
old_col.set_title();
destination_column.set_title();
if (this.delegate.get_current_todo_file().automatic_sort)
this._sort_task_in_column(task.actor_parent, task);
}
// We don't want to refresh the entire view after the tasks have been
// sorted; we only need to put the dragged task in the right position.
_sort_task_in_column (container, task) {
let tasks = this.delegate.tasks;
let idx = tasks.indexOf(task);
let sorted = false;
for (let i = idx+1; i < tasks.length; i++) {
let it = tasks[i].actor;
if (container.contains(it)) {
container.set_child_below_sibling(task.actor, it);
sorted = true;
break;
}
}
if (! sorted) {
container.remove_child(task.actor);
container.add_child(task.actor);
}
}
_sort_task_in_arrays (task) {
if (this.tasks_scroll_content.get_n_children() < 2) return;
let above = true;
let relative = task.actor.get_next_sibling();
if (! relative) {
above = false;
relative = task.actor.get_previous_sibling();
}
for (let arr of [this.owner.tasks_viewport, this.delegate.tasks]) {
for (let i = 0; i < arr.length; i++) {
if (arr[i] === task) {
arr.splice(i, 1);
break;
}
}
for (let i = 0; i < arr.length; i++) {
if (arr[i].actor === relative) {
if (above) arr.splice(i, 0, task);
else arr.splice(i+1, 0, task);
break;
}
}
}
}
// When the user drags a task from one column to another:
// - remove all properties in the task (priority, context, proj) that
// would make the task go into any column between the old and new.
// - ensure that the task has the property that will make it go into
// the new column.
//
// In some cases it is not possible to ensure that the task will not be put
// into a column that the user didn't drag it into.
// E.g.,
// - User dragged from col1 to col3 but col2 is a kitchen sink.
// - User dragged from col1 (a priority column) into col3 not a priority
// column but col2 is (_). When we remove priority (A) from the task, it
// will end up in col2.
//
// For this reason, we return [@old_col, @new_col, @destination_column]
// @old_col : column in which the task used to be in
// @new_col : column into which the user dropped the task
// @destination_column : column in which the task ended up
_update_task_props (old_parent, new_parent, task) {
let old_col, new_col, idx_old, idx_new;
let children = this.owner.content_box.get_children();
for (let i = 0; i < children.length; i++) {
let it = children[i]._owner;
if (it.tasks_scroll_content === old_parent) {
old_col = it;
idx_old = i;
} else if (it.tasks_scroll_content === new_parent) {
new_col = it;
idx_new = i;
}
}
let new_task_str = task.task_str;
// ensure new prop
if (! new_col.is_kitchen_sink) {
let prop = new_col.filters[0];
if (REG.TODO_CONTEXT.test(prop) && task.contexts.indexOf(prop) === -1) {
new_task_str = new_task_str + ' ' + prop;
}
else if (REG.TODO_PROJ.test(prop) && task.projects.indexOf(prop) === -1) {
new_task_str = new_task_str + ' ' + prop;
}
else if (prop === '(_)' || REG.TODO_PRIO.test(prop)) {
task.priority = prop;
new_task_str = task.new_str_for_prio(prop);
}
}
// remove old props
let kitchen_sink_col = [null, -1];
let no_prio_col = [null, -1];
let ltr = idx_old < idx_new;
let [i, len] = ltr ? [idx_old, idx_new] : [idx_new, idx_old];
for (; i <= len; i++) {
let it = this.owner.content_box.get_child_at_index(i)._owner;
if (it === new_col) continue;
if (it.is_kitchen_sink) {
if (!kitchen_sink_col[0] && ltr) kitchen_sink_col = [it, i];
continue;
}
for (let f of it.filters) {
if (REG.TODO_CONTEXT.test(f) && task.contexts.indexOf(f) !== -1) {
new_task_str = new_task_str.replace(new RegExp(`(^| )\\${f}`, 'g'), '');
}
else if (REG.TODO_PROJ.test(f) && task.projects.indexOf(f) !== -1) {
new_task_str = new_task_str.replace(new RegExp(`(^| )\\${f}`, 'g'), '');
}
else if (REG.TODO_PRIO.test(f) && task.priority === f) {
new_task_str = task.new_str_for_prio('(_)', new_task_str);
}
else if (f === '(_)' && ltr && !no_prio_col[0]) {
no_prio_col = [it, i];
}
}
}
task.reset(true, new_task_str.trim());
if (task.priority !== '(_)') no_prio_col = [null, -1];
let destination_column;
let i1 = kitchen_sink_col[1];
let i2 = no_prio_col[1];
if (i1 === i2) destination_column = new_col;
else if (i1 < 0) destination_column = no_prio_col[0];
else if (i2 < 0) destination_column = kitchen_sink_col[0];
else if (i1 < i2) destination_column = kitchen_sink_col[0];
else destination_column = no_prio_col[0];
return [old_col, new_col, destination_column];
}
handleDragOver (source, drag_actor, x, y, time) {
if (source.dnd_group !== G.DNDGroup.TASK) return DND.DragMotionResult.CONTINUE;
if (source.item.actor_parent === this.tasks_scroll_content) return DND.DragMotionResult.CONTINUE;
source.item.actor_parent.remove_child(source.item.actor);
source.item.actor_parent = this.tasks_scroll_content;
this.tasks_scroll_content.add_child(source.item.actor);
return DND.DragMotionResult.MOVE_DROP;
}
close () {
this.sigm.clear();
}
}
Signals.addSignalMethods(KanbanColumn.prototype);

View File

@@ -0,0 +1,532 @@
const St = imports.gi.St;
const Gtk = imports.gi.Gtk;
const GLib = imports.gi.GLib;
const Pango = imports.gi.Pango;
const Clutter = imports.gi.Clutter;
const Main = imports.ui.main;
const Signals = imports.signals;
const Mainloop = imports.mainloop;
const ME = imports.misc.extensionUtils.getCurrentExtension();
const Gettext = imports.gettext.domain(ME.metadata['gettext-domain']);
const _ = Gettext.gettext;
const ngettext = Gettext.ngettext;
const FUZZ = ME.imports.lib.fuzzy_search;
const MULTIL_ENTRY = ME.imports.lib.multiline_entry;
const MISC_UTILS = ME.imports.lib.misc_utils;
const REG = ME.imports.lib.regex;
const TEXT_LINKS_MNGR = ME.imports.lib.text_links_manager;
const G = ME.imports.sections.todo.GLOBAL;
// =====================================================================
// @@@ ViewFileSwitcher
//
// @ext : obj (main extension object)
// @delegate : obj (main section object)
//
// @signals:
// - 'update'
// =====================================================================
var ViewFileSwitcher = class ViewFileSwitcher {
constructor (ext, delegate) {
this.ext = ext;
this.delegate = delegate;
this.linkm = new TEXT_LINKS_MNGR.TextLinksManager();
this.file_items = new Set();
this.file_info_editor = null;
//
// container
//
this.actor = new St.BoxLayout({ x_expand: true, vertical: true, style_class: 'view-file-switcher view-box' });
this.content_box = new St.BoxLayout({ x_expand: true, vertical: true, style_class: 'view-box-content' });
this.actor.add_child(this.content_box);
//
// search files entry
//
this.entry_box = new St.BoxLayout({ style_class: 'row' });
this.content_box.add(this.entry_box);
this.entry_box.visible = this.delegate.cache.todo_files.length > 0;
this.entry = new St.Entry({ hint_text: _('Search...'), can_focus: true, x_expand: true, name: 'menu-search-entry' });
this.entry_box.add_child(this.entry);
//
// file items container
//
this.file_items_scrollview = new St.ScrollView({ style_class: 'vfade' });
this.content_box.add_actor(this.file_items_scrollview);
this.file_items_scrollview.visible = this.delegate.cache.todo_files.length > 0;
this.file_items_scrollview.vscrollbar_policy = Gtk.PolicyType.NEVER;
this.file_items_scrollview.hscrollbar_policy = Gtk.PolicyType.NEVER;
this.file_items_scrollbox = new St.BoxLayout({ vertical: true, style_class: 'row' });
this.file_items_scrollview.add_actor(this.file_items_scrollbox);
for (let file of this.delegate.cache.todo_files) {
this._add_new_file_item(file);
}
//
// buttons
//
let btn_box = new St.BoxLayout({ x_expand: true, style_class: 'row btn-box' });
this.content_box.add_child(btn_box);
this.button_add_file = new St.Button({ can_focus: true, label: _('Add File'), style_class: 'button', x_expand: true });
btn_box.add(this.button_add_file, {expand: true});
this.button_cancel = new St.Button({ can_focus: true, label: _('Cancel'), style_class: 'btn-cancel button', x_expand: true });
btn_box.add(this.button_cancel, {expand: true});
this.button_cancel.visible = this.delegate.cache.todo_files.length > 0;
this.button_ok = new St.Button({ can_focus: true, label: _('Ok'), style_class: 'btn-ok button', x_expand: true });
this.button_ok.visible = this.delegate.cache.todo_files.length > 0;
btn_box.add(this.button_ok, {expand: true});
//
// listen
//
this.file_items_scrollbox.connect('allocation-changed', () => {
this.file_items_scrollview.vscrollbar_policy = Gtk.PolicyType.NEVER;
if (ext.needs_scrollbar()) this.file_items_scrollview.vscrollbar_policy = Gtk.PolicyType.ALWAYS;
});
this.entry.clutter_text.connect('text-changed', () => this._search());
this.button_add_file.connect('clicked', () => this._show_file_editor());
this.button_cancel.connect('clicked', () => this.emit('cancel'));
this.button_ok.connect('clicked', () => this._on_file_selected());
this.entry.clutter_text.connect('activate', () => this._select_first());
}
_search () {
this.file_items_scrollbox.remove_all_children();
let needle = this.entry.get_text().toLowerCase();
if (!needle) {
for (let it of this.file_items)
this.file_items_scrollbox.add_child(it.actor);
} else {
let reduced_results = [];
for (let it of this.file_items) {
let msg = (it.file.name + it.file.todo_file + it.file.done_file + it.file.time_tracker_dir).toLowerCase();
let score = FUZZ.fuzzy_search_v1(needle, msg);
if (score) reduced_results.push([score, it]);
}
reduced_results.sort((a, b) => a[0] < b[0]);
for (let it of reduced_results)
this.file_items_scrollbox.add_child(it[1].actor);
}
}
_show_file_editor (item) {
if (this.file_info_editor) this.file_info_editor.close();
this.file_info_editor = new FileInfoEditor(this.ext, this.delegate, item ? item.file : null);
this.actor.add_child(this.file_info_editor.actor);
let is_active = item && item.active;
this.file_info_editor.button_cancel.grab_key_focus();
this.content_box.hide();
this.file_info_editor.connect('ok', (_, file) => {
if (item) {
this.file_items.delete(item);
item.actor.destroy();
} else {
file.active = true;
for (let it of this.file_items) {
if (it.active) {
file.active = false;
break;
}
}
}
this._add_new_file_item(file);
this.content_box.show();
this.entry_box.show();
this.file_items_scrollview.show();
this.button_cancel.show();
this.button_ok.show();
this.button_ok.grab_key_focus();
this.file_info_editor.close();
this.file_info_editor = null;
});
this.file_info_editor.connect('delete', () => {
this.file_items.delete(item);
item.actor.destroy();
if (is_active) {
for (let it of this.file_items) {
it.file.active = true;
it.check_icon.add_style_class_name('active');
it.check_icon.show();
break;
}
}
this.content_box.show();
this.entry.grab_key_focus();
this.file_info_editor.close();
this.file_info_editor = null;
});
this.file_info_editor.connect('cancel', () => {
this.content_box.show();
this.entry.grab_key_focus();
this.file_info_editor.close();
this.file_info_editor = null;
});
}
_add_new_file_item (file) {
let item = {};
this.file_items.add(item);
item.file = file;
item.actor = new St.BoxLayout({ can_focus: true, reactive: true, vertical: true, style_class: 'file-switcher-item' });
item.actor._delegate = item;
item.header = new St.BoxLayout();
item.actor.add_child(item.header);
item.header.add_child(new St.Label({ text: file.name, x_expand: true, y_align: Clutter.ActorAlign.CENTER, style_class: 'file-switcher-item-title' }));
item.icon_box = new St.BoxLayout({ style_class: 'icon-box' });
item.header.add_child(item.icon_box);
item.check_icon = new St.Icon({ visible: false, track_hover: true, can_focus: true, reactive: true, gicon : MISC_UTILS.getIcon('timepp-todo-symbolic'), style_class: 'file-switcher-item-check-icon' });
item.icon_box.add_child(item.check_icon);
let edit_icon = new St.Icon({ visible: false, track_hover: true, can_focus: true, reactive: true, gicon : MISC_UTILS.getIcon('timepp-edit-symbolic') });
item.icon_box.add_child(edit_icon);
{
item.msg = new St.Label({ y_align: Clutter.ActorAlign.CENTER });
item.actor.add_child(item.msg);
item.msg.clutter_text.line_wrap = true;
item.msg.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
item.msg.clutter_text.line_wrap_mode = Pango.WrapMode.WORD_CHAR;
let markup = file.todo_file;
if (file.done_file) markup += '\n' + file.done_file;
if (file.time_tracker_dir) markup += '\n' + file.time_tracker_dir;
this.linkm.add_label_actor(item.msg, new Map([[REG.FILE_PATH, MISC_UTILS.open_file_path]]));
item.msg.clutter_text.set_markup(this.highlight_tokens(markup));
}
if (file.active) {
item.active = true;
item.check_icon.show();
item.check_icon.add_style_class_name('active');
this.file_items_scrollbox.insert_child_at_index(item.actor, 0);
} else {
item.active = false;
this.file_items_scrollbox.add_child(item.actor);
}
// listen
this.delegate.sigm.connect_release(item.check_icon, Clutter.BUTTON_PRIMARY, true, () => this._on_file_selected(file));
this.delegate.sigm.connect_release(edit_icon, Clutter.BUTTON_PRIMARY, true, () => this._show_file_editor(item));
item.actor.connect('key-focus-in', () => { item.actor.can_focus = false; });
item.actor.connect('event', (_, event) => this._on_file_item_event(item, event));
return item;
}
_select_first () {
let c = this.file_items_scrollbox.get_first_child();
if (!c) return;
this._on_file_selected(c._delegate.file);
}
_on_file_selected (file) {
let files = [];
for (let it of this.file_items) {
if (file) it.file.active = (it.file.name === file.name);
files.push(it.file);
}
this.emit('update', files);
}
highlight_tokens (text) {
text = GLib.markup_escape_text(text, -1);
text = MISC_UTILS.markdown_to_pango(text, this.ext.markdown_map);
text = MISC_UTILS.split_on_whitespace(text);
let token;
for (let i = 0; i < text.length; i++) {
token = text[i];
if (! REG.FILE_PATH.test(token)) continue;
text[i] =
'<span foreground="' + this.ext.custom_css['-timepp-link-color'][0] +
'"><u><b>' + token + '</b></u></span>';
}
return text.join('');
}
_on_file_item_event (item, event) {
switch (event.type()) {
case Clutter.EventType.ENTER: {
let related = event.get_related();
if (related && !item.actor.contains(related)) {
for (let it of item.icon_box.get_children()) it.show();
}
} break;
case Clutter.EventType.LEAVE: {
let related = event.get_related();
if (!item.header.contains(global.stage.get_key_focus()) && related && !item.actor.contains(related)) {
for (let it of item.icon_box.get_children()) it.hide();
item.check_icon.visible = item.active;
item.actor.can_focus = true;
}
} break;
case Clutter.EventType.KEY_RELEASE: {
for (let it of item.icon_box.get_children()) it.show();
if (!item.header.contains(global.stage.get_key_focus())) item.icon_box.get_first_child().grab_key_focus();
MISC_UTILS.scroll_to_item(this.file_items_scrollview, this.file_items_scrollbox, item.actor);
item.actor.can_focus = false;
} break;
case Clutter.EventType.KEY_PRESS: {
Mainloop.idle_add(() => {
if (item.icon_box && !item.header.contains(global.stage.get_key_focus())) {
item.actor.can_focus = true;
for (let it of item.icon_box.get_children()) it.hide();
item.check_icon.visible = item.active;
}
});
} break;
}
}
close () {
if (this.file_info_editor) this.file_info_editor.close();
this.file_info_editor = null;
this.actor.destroy();
}
}
Signals.addSignalMethods(ViewFileSwitcher.prototype);
// =====================================================================
// @@@ FileInfoEditor
//
// @ext : obj (main extension object)
// @delegate : obj (main section object)
// @file : obj
//
// @signals: 'ok', 'cancel', 'delete'
// =====================================================================
var FileInfoEditor = class FileInfoEditor {
constructor (ext, delegate, file) {
this.ext = ext;
this.delegate = delegate;
this.file = file;
this.todo_file_chooser_proc = null;
this.done_file_chooser_proc = null;
this.tracker_file_chooser_proc = null;
//
// container
//
this.actor = new St.BoxLayout({ x_expand: true, vertical: true, style_class: 'view-box-content' });
// unique name
{
let row = new St.Bin({ style_class: 'row' });
this.actor.add_child(row);
this.name_entry = new St.Entry({ hint_text: _('Unique name'), can_focus: true });
row.add_actor(this.name_entry);
if (file) this.name_entry.text = file.name;
}
// todo file path
{
let row = new St.Bin({ style_class: 'row' });
this.actor.add_child(row);
this.todo_entry = new St.Entry({ hint_text: _('Todo file'), can_focus: true });
row.add_actor(this.todo_entry);
this.todo_search_icon = new St.Icon({ track_hover: true, reactive: true, gicon : MISC_UTILS.getIcon('timepp-search-symbolic') });
this.todo_entry.set_secondary_icon(this.todo_search_icon);
if (file) this.todo_entry.text = file.todo_file;
}
// done file path
{
let row = new St.Bin({ style_class: 'row' });
this.actor.add_child(row);
let hint = `${_('Done file')} (${_('optional')})`;
this.done_entry = new St.Entry({ hint_text: hint, can_focus: true });
row.add_actor(this.done_entry);
this.done_search_icon = new St.Icon({ track_hover: true, reactive: true, gicon : MISC_UTILS.getIcon('timepp-search-symbolic') });
this.done_entry.set_secondary_icon(this.done_search_icon);
if (file) this.done_entry.text = file.done_file;
}
// time tracker dir path
{
let row = new St.Bin({ style_class: 'row' });
this.actor.add_child(row);
let hint = `${_('Time-tracker directory')} (${_('optional')})`;
this.tracker_entry = new St.Entry({ hint_text: hint, can_focus: true });
row.add_actor(this.tracker_entry);
this.tracker_search_icon = new St.Icon({ track_hover: true, reactive: true, gicon : MISC_UTILS.getIcon('timepp-search-symbolic') });
this.tracker_entry.set_secondary_icon(this.tracker_search_icon);
if (file) this.tracker_entry.text = file.time_tracker_dir;
}
//
// buttons
//
let btn_box = new St.BoxLayout({ style_class: 'row btn-box' });
this.actor.add(btn_box, {expand: true});
if (file) {
this.button_delete = new St.Button({ can_focus: true, label: _('Delete'), style_class: 'btn-delete button', x_expand: true });
btn_box.add(this.button_delete, {expand: true});
this.button_delete.connect('clicked', () => this.emit('delete'));
}
this.button_cancel = new St.Button({ can_focus: true, label: _('Cancel'), style_class: 'btn-cancel button', x_expand: true });
this.button_ok = new St.Button({ can_focus: true, label: _('Ok'), style_class: 'btn-ok button', x_expand: true });
btn_box.add(this.button_cancel, {expand: true});
btn_box.add(this.button_ok, {expand: true});
this._update_ok_btn();
//
// listen
//
this.todo_search_icon.connect('button-press-event', () => {
if (this.todo_file_chooser_proc) this.todo_file_chooser_proc.force_exit();
this.ext.menu.close();
this.todo_file_chooser_proc = MISC_UTILS.open_file_dialog(false, (out) => {
this.todo_entry.set_text(out);
this.todo_file_chooser_proc = null;
this.ext.menu.open();
});
});
this.done_search_icon.connect('button-press-event', () => {
if (this.done_file_chooser_proc) this.done_file_chooser_proc.force_exit();
this.ext.menu.close();
this.done_file_chooser_proc = MISC_UTILS.open_file_dialog(false, (out) => {
this.done_entry.set_text(out);
this.done_file_chooser_proc = null;
this.ext.menu.open();
});
});
this.tracker_search_icon.connect('button-press-event', () => {
if (this.tracker_file_chooser_proc) this.tracker_file_chooser_proc.force_exit();
this.ext.menu.close();
this.tracker_file_chooser_proc = MISC_UTILS.open_file_dialog(true, (out) => {
this.tracker_entry.set_text(out);
this.tracker_file_chooser_proc = null;
this.ext.menu.open();
});
});
this.button_ok.connect('clicked', () => this._on_ok_clicked());
this.button_cancel.connect('clicked', () => this.emit('cancel'));
this.name_entry.clutter_text.connect('text-changed', () => this._update_ok_btn());
this.todo_entry.clutter_text.connect('text-changed', () => this._update_ok_btn());
}
_on_ok_clicked () {
let file = this.file;
if (! file) file = G.TODO_RECORD();
file.name = this.name_entry.text,
file.todo_file = this.todo_entry.text,
file.done_file = this.done_entry.text,
file.time_tracker_dir = this.tracker_entry.text,
file.automatic_sort = file ? file.automatic_sort : true,
this.emit('ok', file);
}
_update_ok_btn () {
if (!this.name_entry.text || !this.todo_entry.text) {
this.button_ok.visible = false;
return;
}
let name = this.name_entry.get_text();
if (this.file && this.file.name === name) {
this.button_ok.visible = true;
return;
}
for (let file of this.delegate.cache.todo_files) {
if (file.name === name) {
this.button_ok.visible = false;
return;
}
}
this.button_ok.visible = true;
}
close () {
if (this.todo_file_chooser_proc) this.todo_file_chooser_proc.force_exit();
if (this.done_file_chooser_proc) this.done_file_chooser_proc.force_exit();
if (this.tracker_file_chooser_proc) this.tracker_file_chooser_proc.force_exit();
this.actor.destroy();
}
}
Signals.addSignalMethods(FileInfoEditor.prototype);

View File

@@ -0,0 +1,457 @@
const St = imports.gi.St;
const Gtk = imports.gi.Gtk;
const Clutter = imports.gi.Clutter;
const Main = imports.ui.main;
const CheckBox = imports.ui.checkBox;
const PopupMenu = imports.ui.popupMenu;
const Signals = imports.signals;
const Mainloop = imports.mainloop;
const ME = imports.misc.extensionUtils.getCurrentExtension();
const Gettext = imports.gettext.domain(ME.metadata['gettext-domain']);
const _ = Gettext.gettext;
const ngettext = Gettext.ngettext;
const MULTIL_ENTRY = ME.imports.lib.multiline_entry;
const MISC_UTILS = ME.imports.lib.misc_utils;
const G = ME.imports.sections.todo.GLOBAL;
// =====================================================================
// @@@ ViewFilters
//
// @ext : obj (main extension object)
// @delegate : obj (main section object)
//
// @signals:
// - 'filters-updated' returns new filters record
// =====================================================================
var ViewFilters = class ViewFilters {
constructor (ext, delegate) {
this.ext = ext;
this.delegate = delegate;
// We store all filter item objects here.
// I.e., those objects created by the _new_filter_item() func.
this.filter_register = {
completed : null,
no_priority : null,
priorities : [],
contexts : [],
projects : [],
custom : [],
};
// Array of PopupMenu.Switch() actors
this.nand_toggles = [];
//
// actor
//
this.actor = new St.Bin({ x_fill: true, style_class: 'view-filters view-box' });
this.content_box = new St.BoxLayout({ x_expand: true, vertical: true, style_class: 'view-box-content' });
this.actor.add_actor(this.content_box);
//
// custom filters entry
//
this.entry = new MULTIL_ENTRY.MultiLineEntry(_('Add custom filter...'), false, true);
this.content_box.add_child(this.entry.actor);
this.entry.actor.add_style_class_name('row');
//
// filters
//
this.filter_sectors_scroll = new St.ScrollView({ style_class: 'vfade' });
this.content_box.add_actor(this.filter_sectors_scroll);
this.filter_sectors_scroll.vscrollbar_policy = Gtk.PolicyType.NEVER;
this.filter_sectors_scroll.hscrollbar_policy = Gtk.PolicyType.NEVER;
this.filter_sectors_scroll_box = new St.BoxLayout({ vertical: true });
this.filter_sectors_scroll.add_actor(this.filter_sectors_scroll_box);
this.custom_filters_box = new St.BoxLayout({ vertical: true, x_expand: true, style_class: 'filter-settings-sector' });
this.filter_sectors_scroll_box.add_actor(this.custom_filters_box);
this.priority_filters_box = new St.BoxLayout({ vertical: true, x_expand: true, style_class: 'filter-settings-sector' });
this.filter_sectors_scroll_box.add_actor(this.priority_filters_box);
this.context_filters_box = new St.BoxLayout({ vertical: true, x_expand: true, style_class: 'filter-settings-sector' });
this.filter_sectors_scroll_box.add_actor(this.context_filters_box);
this.project_filters_box = new St.BoxLayout({ vertical: true, x_expand: true, style_class: 'filter-settings-sector' });
this.filter_sectors_scroll_box.add_actor(this.project_filters_box);
this._add_separator(this.content_box);
//
// toggles sector
//
this.toggles_sector = new St.BoxLayout({ vertical: true, x_expand: true, style_class: 'filter-settings-sector' });
this.content_box.add_child(this.toggles_sector);
//
// show hidden only switch
//
this.show_hidden_tasks_item = new St.BoxLayout({ reactive: true, style_class: 'row filter-window-item' });
this.toggles_sector.add_child(this.show_hidden_tasks_item);
let show_hidden_tasks_label = new St.Label({ text: _('Show hidden tasks only'), y_align: Clutter.ActorAlign.CENTER });
this.show_hidden_tasks_item.add(show_hidden_tasks_label, {expand: true});
let hidden_count_label = new St.Label({ y_align: Clutter.ActorAlign.CENTER, style_class: 'popup-inactive-menu-item', pseudo_class: 'insensitive' });
this.show_hidden_tasks_item.add_child(hidden_count_label);
hidden_count_label.text =
ngettext('%d hidden task', '%d hidden tasks', this.delegate.stats.hidden)
.format(this.delegate.stats.hidden);
this.show_hidden_tasks_toggle_btn = new St.Button({ can_focus: true });
this.show_hidden_tasks_item.add_actor(this.show_hidden_tasks_toggle_btn);
this.show_hidden_tasks_toggle = new PopupMenu.Switch();
this.nand_toggles.push(this.show_hidden_tasks_toggle);
this.show_hidden_tasks_toggle_btn.add_actor(this.show_hidden_tasks_toggle.actor);
//
// show recurring only switch
//
this.show_recurring_tasks_item = new St.BoxLayout({ reactive: true, style_class: 'row filter-window-item' });
this.toggles_sector.add_child(this.show_recurring_tasks_item);
let show_recurring_tasks_label = new St.Label({ text: _('Show recurring tasks only'), y_align: Clutter.ActorAlign.CENTER });
this.show_recurring_tasks_item.add(show_recurring_tasks_label, {expand: true});
let recurring_count_label = new St.Label({ y_align: Clutter.ActorAlign.CENTER, style_class: 'popup-inactive-menu-item', pseudo_class: 'insensitive' });
this.show_recurring_tasks_item.add_child(recurring_count_label);
let n_recurring = this.delegate.stats.recurring_completed +
this.delegate.stats.recurring_incompleted;
recurring_count_label.text =
ngettext('%d recurring task', '%d recurring tasks', n_recurring)
.format(n_recurring);
this.show_recurring_tasks_toggle_btn = new St.Button({ can_focus: true });
this.show_recurring_tasks_item.add_actor(this.show_recurring_tasks_toggle_btn);
this.show_recurring_tasks_toggle = new PopupMenu.Switch();
this.nand_toggles.push(this.show_recurring_tasks_toggle);
this.show_recurring_tasks_toggle_btn.add_actor(this.show_recurring_tasks_toggle.actor);
//
// show deferred tasks only switch
//
this.show_deferred_tasks_item = new St.BoxLayout({ reactive: true, style_class: 'row filter-window-item' });
this.toggles_sector.add_child(this.show_deferred_tasks_item);
let show_deferred_tasks_label = new St.Label({ text: _('Show deferred tasks only'), y_align: Clutter.ActorAlign.CENTER });
this.show_deferred_tasks_item.add(show_deferred_tasks_label, {expand: true});
let deferred_count_label = new St.Label({ y_align: Clutter.ActorAlign.CENTER, style_class: 'popup-inactive-menu-item', pseudo_class: 'insensitive' });
this.show_deferred_tasks_item.add_child(deferred_count_label);
let n_deferred = this.delegate.stats.deferred_tasks;
deferred_count_label.text =
ngettext('%d deferred task', '%d deferred tasks', n_deferred)
.format(n_deferred);
this.show_deferred_tasks_toggle_btn = new St.Button({ can_focus: true });
this.show_deferred_tasks_item.add_actor(this.show_deferred_tasks_toggle_btn);
this.show_deferred_tasks_toggle = new PopupMenu.Switch();
this.nand_toggles.push(this.show_deferred_tasks_toggle);
this.show_deferred_tasks_toggle_btn.add_actor(this.show_deferred_tasks_toggle.actor);
//
// Invert switch (whitelist/blacklist)
//
this.invert_item = new St.BoxLayout({ reactive: true, style_class: 'row filter-window-item' });
this.toggles_sector.add_child(this.invert_item);
let invert_label = new St.Label({ text: _('Invert filters'), y_align: St.Align.END });
this.invert_item.add(invert_label, {expand: true});
this.invert_toggle_btn = new St.Button({ can_focus: true });
this.invert_item.add_actor(this.invert_toggle_btn);
this.invert_toggle = new PopupMenu.Switch();
this.invert_toggle_btn.add_actor(this.invert_toggle.actor);
//
// buttons
//
this.btn_box = new St.BoxLayout({ x_expand: true, style_class: 'row btn-box' });
this.content_box.add_child(this.btn_box);
this.button_reset = new St.Button({ can_focus: true, label: _('Reset'), style_class: 'button' });
this.button_ok = new St.Button({ can_focus: true, label: _('Ok'), style_class: 'btn-ok button' });
this.btn_box.add(this.button_reset, {expand: true});
this.btn_box.add(this.button_ok, {expand: true});
//
// load filter items
//
this._load_filters();
//
// listen
//
this.entry.entry.clutter_text.connect('activate', () => {
if (! this.entry.entry.get_text()) return;
// check for duplicates
for (let i = 0; i < this.filter_register.custom.length; i++) {
if (this.filter_register.custom[i].filter === this.entry.entry.get_text())
return;
}
let item = this._new_filter_item(true, this.entry.entry.get_text(), false,
true, this.custom_filters_box);
this.custom_filters_box.add_child(item.actor);
this.filter_register.custom.push(item);
this.entry.entry.text = '';
});
this.filter_sectors_scroll_box.connect('allocation-changed', () => {
this.filter_sectors_scroll.vscrollbar_policy = Gtk.PolicyType.NEVER;
if (this.ext.needs_scrollbar()) this.filter_sectors_scroll.vscrollbar_policy = Gtk.PolicyType.ALWAYS;
});
this.show_hidden_tasks_toggle_btn.connect('clicked', () => this._on_nand_toggle_clicked(this.show_hidden_tasks_toggle));
this.show_hidden_tasks_item.connect('button-press-event', () => this._on_nand_toggle_clicked(this.show_hidden_tasks_toggle));
this.show_deferred_tasks_toggle_btn.connect('clicked', () => this._on_nand_toggle_clicked(this.show_deferred_tasks_toggle));
this.show_deferred_tasks_item.connect('button-press-event', () => this._on_nand_toggle_clicked(this.show_deferred_tasks_toggle));
this.show_recurring_tasks_toggle_btn.connect('clicked', () => this._on_nand_toggle_clicked(this.show_recurring_tasks_toggle));
this.show_recurring_tasks_item.connect('button-press-event', () => this._on_nand_toggle_clicked(this.show_recurring_tasks_toggle));
this.invert_toggle_btn.connect('clicked', () => this.invert_toggle.toggle());
this.invert_item.connect('button-press-event', () => this.invert_toggle.toggle());
this.button_reset.connect('clicked', () => this._reset_all());
this.button_ok.connect('clicked', () => this._on_ok_clicked());
}
_load_filters () {
let filters = this.delegate.get_current_todo_file().filters;
this.invert_toggle.setToggleState(filters.invert_filters);
this.show_hidden_tasks_toggle.setToggleState(filters.hidden);
this.show_deferred_tasks_toggle.setToggleState(filters.deferred);
this.show_recurring_tasks_toggle.setToggleState(filters.recurring);
// custom filters
for (let i = 0, len = filters.custom.length; i < len; i++) {
let value = filters.custom[i];
let check = filters.custom_active.indexOf(value) === -1 ? false : true;
let item = this._new_filter_item(check, value, 0, true, this.custom_filters_box);
this.custom_filters_box.add_child(item.actor);
this.filter_register.custom.push(item);
}
this._add_separator(this.priority_filters_box);
// completed
if (this.delegate.stats.completed > 0) {
let item = this._new_filter_item(filters.completed, _('Completed'), this.delegate.stats.completed, 0, this.priority_filters_box);
this.filter_register.completed = item;
this.priority_filters_box.add_child(item.actor);
}
// no priority
if (this.delegate.stats.no_priority > 0) {
let item = this._new_filter_item(filters.no_priority, _('No Priority'), this.delegate.stats.no_priority, 0, this.priority_filters_box);
this.filter_register.no_priority = item;
this.priority_filters_box.add_child(item.actor);
}
// priorities
for (let [key, value] of this.delegate.stats.priorities) {
let check = filters.priorities.indexOf(key) === -1 ? false : true;
this.filter_register.priorities.push(
this._new_filter_item(check, key, value, false, this.priority_filters_box));
}
this.filter_register.priorities.sort((a, b) => {
return +(a.filter > b.filter) || +(a.filter === b.filter) - 1;
});
for (let i = 0; i < this.filter_register.priorities.length; i++) {
this.priority_filters_box.add_child(this.filter_register.priorities[i].actor);
}
this._add_separator(this.context_filters_box);
// contexts
for (let [key, value] of this.delegate.stats.contexts) {
let check = filters.contexts.indexOf(key) === -1 ? false : true;
let item = this._new_filter_item(check, key, value, false, this.context_filters_box);
this.context_filters_box.add_child(item.actor);
this.filter_register.contexts.push(item);
}
this._add_separator(this.project_filters_box);
// projects
for (let [key, value] of this.delegate.stats.projects) {
let check = filters.projects.indexOf(key) === -1 ? false : true;
let item = this._new_filter_item(check, key, value, false, this.project_filters_box);
this.project_filters_box.add_child(item.actor);
this.filter_register.projects.push(item);
}
// hide the sections that don't have any items
[
this.priority_filters_box,
this.context_filters_box,
this.project_filters_box,
].forEach((it) => it.get_n_children() === 1 && it.hide());
}
_reset_all () {
if (this.filter_register.completed)
this.filter_register.completed.checkbox.actor.checked = false;
if (this.filter_register.no_priority)
this.filter_register.no_priority.checkbox.actor.checked = false;
[
this.filter_register.priorities,
this.filter_register.contexts,
this.filter_register.projects,
this.filter_register.custom,
].forEach((arr) => {
for (let i = 0; i < arr.length; i++)
arr[i].checkbox.actor.checked = false;
});
}
_new_filter_item (is_checked, label, count, is_deletable, parent_box) {
let item = {};
item.actor = new St.BoxLayout({ reactive: true, style_class: 'row filter-window-item' });
item.filter = label;
item.label = new St.Label({ text: label, x_expand: true, y_align: Clutter.ActorAlign.CENTER });
item.actor.add_child(item.label);
if (count) {
item.count_label = new St.Label({ y_align: Clutter.ActorAlign.CENTER, style_class: 'popup-inactive-menu-item', pseudo_class: 'insensitive' });
item.actor.add_child(item.count_label);
item.count_label.text =
ngettext('%d task', '%d tasks', count).format(count) + ' ';
}
item.checkbox = new CheckBox.CheckBox();
item.actor.add_actor(item.checkbox.actor);
item.checkbox.actor.checked = is_checked;
item.checkbox.actor.y_align = St.Align.MIDDLE;
if (is_deletable) {
let close_button = new St.Button({ can_focus: true, style_class: 'close-icon' });
item.actor.add_actor(close_button);
close_button.add_actor(new St.Icon({ gicon : MISC_UTILS.getIcon('timepp-close-symbolic') }));
close_button.connect('clicked', () => this._delete_custom_item(item));
close_button.connect('key-focus-in', () => MISC_UTILS.scroll_to_item(this.filter_sectors_scroll, this.filter_sectors_scroll_box, item.actor, parent_box));
}
item.actor.connect('button-press-event', () => { item.checkbox.actor.checked = !item.checkbox.actor.checked; });
item.checkbox.actor.connect('key-focus-in', () => MISC_UTILS.scroll_to_item(this.filter_sectors_scroll, this.filter_sectors_scroll_box, item.actor, parent_box));
return item;
}
_delete_custom_item (item) {
if (item.checkbox.actor.has_key_focus || close_button.has_key_focus)
this.entry.entry.grab_key_focus();
item.actor.destroy();
for (let i = 0; i < this.filter_register.custom.length; i++) {
if (this.filter_register.custom[i] === item) {
this.filter_register.custom.splice(i, 1);
return;
}
}
}
_add_separator (container) {
let sep = new PopupMenu.PopupSeparatorMenuItem();
sep.actor.add_style_class_name('timepp-separator');
container.add_child(sep.actor);
}
_on_nand_toggle_clicked (toggle_actor) {
if (toggle_actor.state) {
toggle_actor.setToggleState(false);
} else {
for (let toggle of this.nand_toggles) toggle.setToggleState(false);
toggle_actor.setToggleState(true);
}
}
_on_ok_clicked () {
let filters = G.FILTER_RECORD();
filters.invert_filters = this.invert_toggle.state;
filters.deferred = this.show_deferred_tasks_toggle.state;
filters.recurring = this.show_recurring_tasks_toggle.state;
filters.hidden = this.show_hidden_tasks_toggle.state;
filters.completed = !!(this.filter_register.completed && this.filter_register.completed.checkbox.actor.checked);
filters.no_priority = !!(this.filter_register.no_priority && this.filter_register.no_priority.checkbox.actor.checked);
for (let i = 0; i < this.filter_register.priorities.length; i++) {
let it = this.filter_register.priorities[i];
if (it.checkbox.actor.checked) filters.priorities.push(it.filter);
}
for (let i = 0; i < this.filter_register.contexts.length; i++) {
let it = this.filter_register.contexts[i];
if (it.checkbox.actor.checked) filters.contexts.push(it.filter);
}
for (let i = 0; i < this.filter_register.projects.length; i++) {
let it = this.filter_register.projects[i];
if (it.checkbox.actor.checked) filters.projects.push(it.filter);
}
for (let i = 0; i < this.filter_register.custom.length; i++) {
let it = this.filter_register.custom[i];
if (it.checkbox.actor.checked) filters.custom_active.push(it.filter);
filters.custom.push(it.filter);
}
this.emit('filters-updated', filters);
}
close () {
this.actor.destroy();
}
}
Signals.addSignalMethods(ViewFilters.prototype);

View File

@@ -0,0 +1,301 @@
const St = imports.gi.St;
const Gtk = imports.gi.Gtk;
const Pango = imports.gi.Pango;
const Clutter = imports.gi.Clutter;
const Main = imports.ui.main;
const Signals = imports.signals;
const Mainloop = imports.mainloop;
const ME = imports.misc.extensionUtils.getCurrentExtension();
const Gettext = imports.gettext.domain(ME.metadata['gettext-domain']);
const _ = Gettext.gettext;
const ngettext = Gettext.ngettext;
const FUZZ = ME.imports.lib.fuzzy_search;
const MISC_UTILS = ME.imports.lib.misc_utils;
const MULTIL_ENTRY = ME.imports.lib.multiline_entry;
const KAN_HELP_LINK = 'https://github.com/zagortenay333/timepp__gnome#todotxt-extensions';
const G = ME.imports.sections.todo.GLOBAL;
// =====================================================================
// @@@ KanbanSwitcher
//
// @ext : obj (main extension object)
// @delegate : obj (main section object)
// =====================================================================
var KanbanSwitcher = class KanbanSwitcher {
constructor (ext, delegate, task) {
this.ext = ext;
this.delegate = delegate;
this.active_kan = null;
this.kan_items = new Set();
//
// container
//
this.actor = new St.BoxLayout({ x_expand: true, vertical: true, style_class: 'viwe-kanban-switcher view-box' });
this.content_box = new St.BoxLayout({ x_expand: true, vertical: true, style_class: 'view-box-content' });
this.actor.add_child(this.content_box);
//
// search files entry
//
this.entry_box = new St.BoxLayout({ vertical: true, style_class: 'row' });
this.content_box.add_child(this.entry_box);
this.entry = new St.Entry({ hint_text: _('Search...'), can_focus: true, x_expand: true, name: 'menu-search-entry' });
this.entry_box.add_child(this.entry);
//
// help label
//
{
this.help_label = new St.Button({ can_focus: true, reactive: true, x_align: St.Align.END, style_class: 'link' });
this.entry_box.insert_child_at_index(this.help_label, 0);
let label = new St.Label({ text: _('syntax help'), style_class: 'popup-inactive-menu-item', pseudo_class: 'insensitive' });
this.help_label.add_actor(label);
}
//
// items
//
this.items_scrollview = new St.ScrollView({ hscrollbar_policy: Gtk.PolicyType.NEVER, vscrollbar_policy: Gtk.PolicyType.NEVER, style_class: 'vfade' });
this.content_box.add_actor(this.items_scrollview);
this.items_scrollbox = new St.BoxLayout({ vertical: true, style_class: 'row' });
this.items_scrollview.add_actor(this.items_scrollbox);
//
// buttons
//
let btn_box = new St.BoxLayout({ x_expand: true, style_class: 'row btn-box' });
this.content_box.add_child(btn_box);
this.button_cancel = new St.Button({ can_focus: true, label: _('Cancel'), style_class: 'btn-cancel button', x_expand: true });
btn_box.add(this.button_cancel, {expand: true});
this.button_cancel.visible = this.delegate.cache.todo_files.length > 0;
//
// listen
//
this.help_label.connect('clicked', () => MISC_UTILS.open_web_uri(KAN_HELP_LINK));
this.items_scrollbox.connect('allocation-changed', () => {
this.items_scrollview.vscrollbar_policy = Gtk.PolicyType.NEVER;
if (ext.needs_scrollbar()) this.items_scrollview.vscrollbar_policy = Gtk.PolicyType.ALWAYS;
});
this.button_cancel.connect('clicked', () => this.delegate.show_view__default());
this.entry.clutter_text.connect('text-changed', () => this._search());
this.entry.clutter_text.connect('activate', () => {
let first = this.items_scrollbox.get_first_child();
if (first) this._on_kanban_selected(first._delegate);
});
//
// finally
//
this._init_items();
}
_init_items () {
for (let it of this.delegate.tasks) {
if (! it.kanban_boards) continue;
for (let str of it.kanban_boards) this._add_new_item(str, it);
}
if (this.kan_items.size === 0) {
this.entry_box.hide();
this.items_scrollview.hide();
let label = new St.Label({ text: _('To use kanban boards, add the kanban todo extension to a task.'), x_expand: true, y_align: Clutter.ActorAlign.CENTER, style_class: 'row' });
this.content_box.insert_child_at_index(label, 0);
this.entry_box.remove_child(this.help_label);
this.content_box.insert_child_at_index(this.help_label, 1);
this.help_label.x_align = St.Align.MIDDLE;
label = this.help_label.get_first_child();
label.style_class = '';
label.pseudo_style_class = '';
label.clutter_text.set_markup(
'<span foreground="' + this.ext.custom_css['-timepp-link-color'][0] +
'"><u><b>' + label.text + '</b></u></span>');
}
}
_add_new_item (kan_str, task) {
let [name, rest, is_active] = this._parse_kan_str(kan_str);
let item = {};
this.kan_items.add(item);
item.task = task;
item.kan_str = kan_str
item.is_active = is_active;
// actor
item.actor = new St.BoxLayout({ can_focus: true, reactive: true, vertical: true, style_class: 'kanban-switcher-item' });
item.actor._delegate = item;
// header
item.header = new St.BoxLayout();
item.actor.add_child(item.header);
item.header.add_child(new St.Label({ text: name, x_expand: true, y_align: Clutter.ActorAlign.CENTER, style_class: 'kanban-switcher-item-title' }));
// icons
item.icon_box = new St.BoxLayout({ style_class: 'icon-box' });
item.header.add_child(item.icon_box);
item.check_icon = new St.Icon({ visible: false, track_hover: true, can_focus: true, reactive: true, gicon : MISC_UTILS.getIcon('timepp-todo-symbolic'), style_class: 'file-switcher-item-check-icon' });
item.icon_box.add_child(item.check_icon);
let edit_icon = new St.Icon({ visible: false, track_hover: true, can_focus: true, reactive: true, gicon : MISC_UTILS.getIcon('timepp-edit-symbolic') });
item.icon_box.add_child(edit_icon);
if (is_active && !this.active_kan) {
this.active_kan = item;
item.check_icon.visible = true;
item.check_icon.add_style_class_name('active');
this.items_scrollbox.insert_child_at_index(item.actor, 0);
} else {
this.items_scrollbox.add_child(item.actor);
}
// columns body
item.msg = new St.Label({ y_align: Clutter.ActorAlign.CENTER });
item.actor.add_child(item.msg);
item.msg.clutter_text.line_wrap = true;
item.msg.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
item.msg.clutter_text.line_wrap_mode = Pango.WrapMode.WORD_CHAR;
let markup = "";
for (let it of rest.split('|')) {
markup += "\n <b>- " + it.replace(/,/g, ', ') + "</b>";
}
item.msg.clutter_text.set_markup(markup.slice(1));
// listen
this.delegate.sigm.connect_release(item.check_icon, Clutter.BUTTON_PRIMARY, true, () => {
this._on_kanban_selected(item);
});
this.delegate.sigm.connect_release(edit_icon, Clutter.BUTTON_PRIMARY, true, () => this._on_edit_clicked(item));
item.actor.connect('event', (_, event) => this._on_item_event(item, event));
}
_parse_kan_str (str) {
let is_active = str[4] === '*';
let name = str.slice((is_active ? 5 : 4), str.indexOf('|'));
let rest = str.slice(str.indexOf('|')+1);
return [name, rest, is_active];
}
_on_kanban_selected (item) {
if (this.active_kan === item) {
this.delegate.show_view__default();
return;
}
if (this.active_kan) {
let task = this.active_kan.task;
let new_kan_str = this.active_kan.kan_str.replace('*', '');
task.reset(true, task.task_str.replace(this.active_kan.kan_str, new_kan_str));
}
{
let task = item.task;
let new_kan_str = 'kan:*' + item.kan_str.slice(4);
task.reset(true, task.task_str.replace(item.kan_str, new_kan_str));
}
this.delegate.on_tasks_changed(true, true);
}
_on_edit_clicked (item) {
this.delegate.show_view__task_editor(item.task);
}
_search () {
this.items_scrollbox.remove_all_children();
let needle = this.entry.get_text().toLowerCase();
if (!needle) {
for (let it of this.kan_items) this.items_scrollbox.add_child(it.actor);
} else {
let reduced_results = [];
for (let it of this.kan_items) {
let score = FUZZ.fuzzy_search_v1(needle, it.kan_str);
if (score) reduced_results.push([score, it]);
}
reduced_results.sort((a, b) => a[0] < b[0]);
for (let it of reduced_results) this.items_scrollbox.add_child(it[1].actor);
}
}
_on_item_event (item, event) {
let event_type = event.type();
if (event_type === Clutter.EventType.ENTER) {
let related = event.get_related();
if (related && !item.actor.contains(related))
for (let it of item.icon_box.get_children()) it.show();
}
else if (event_type === Clutter.EventType.LEAVE) {
let related = event.get_related();
if (!item.header.contains(global.stage.get_key_focus()) && related && !item.actor.contains(related)) {
for (let it of item.icon_box.get_children()) it.hide();
item.check_icon.visible = item.is_active;
item.actor.can_focus = true;
}
}
else if (event_type === Clutter.EventType.KEY_RELEASE) {
for (let it of item.icon_box.get_children()) it.show();
if (!item.header.contains(global.stage.get_key_focus())) item.icon_box.get_first_child().grab_key_focus();
MISC_UTILS.scroll_to_item(this.items_scrollview, this.items_scrollbox, item.actor);
item.actor.can_focus = false;
}
else if (event_type === Clutter.EventType.KEY_PRESS) {
Mainloop.idle_add(() => {
if (item.icon_box && !item.header.contains(global.stage.get_key_focus())) {
item.actor.can_focus = true;
for (let it of item.icon_box.get_children()) it.hide();
item.check_icon.visible = item.is_active;
}
});
}
}
close () {
for (let it of this.kan_items) it.task = null;
this.active_kan = null;
this.kan_items.clear();
this.actor.destroy();
}
}
Signals.addSignalMethods(KanbanSwitcher.prototype);

View File

@@ -0,0 +1,43 @@
const St = imports.gi.St;
const Main = imports.ui.main;
const Signals = imports.signals;
const Mainloop = imports.mainloop;
const ME = imports.misc.extensionUtils.getCurrentExtension();
const Gettext = imports.gettext.domain(ME.metadata['gettext-domain']);
const _ = Gettext.gettext;
const ngettext = Gettext.ngettext;
// =====================================================================
// @@@ ViewLoading
//
// @ext : obj (main extension object)
// @delegate : obj (main section object)
// =====================================================================
var ViewLoading = class ViewLoading {
constructor (ext, delegate) {
this.ext = ext;
this.delegate = delegate;
this.view_lock = true;
//
// draw
//
this.actor = new St.BoxLayout({ x_expand: true, style_class: 'view-loading timepp-menu-item' });
this.loading_msg = new St.Label({ text: _('Loading...'), style_class: 'loading-msg' });
this.actor.add_child(this.loading_msg);
}
close () {
this.actor.destroy();
}
}
Signals.addSignalMethods(ViewLoading.prototype);

View File

@@ -0,0 +1,236 @@
const St = imports.gi.St;
const Gtk = imports.gi.Gtk;
const Clutter = imports.gi.Clutter;
const Main = imports.ui.main;
const Signals = imports.signals;
const Mainloop = imports.mainloop;
const ME = imports.misc.extensionUtils.getCurrentExtension();
const Gettext = imports.gettext.domain(ME.metadata['gettext-domain']);
const _ = Gettext.gettext;
const ngettext = Gettext.ngettext;
const MISC_UTILS = ME.imports.lib.misc_utils;
const FUZZ = ME.imports.lib.fuzzy_search;
const G = ME.imports.sections.todo.GLOBAL;
// =====================================================================
// @@@ ViewSearch
//
// @ext : obj (main extension object)
// @delegate : obj (main section object)
//
// @signals:
// =====================================================================
var ViewSearch = class ViewSearch {
constructor (ext, delegate) {
this.ext = ext;
this.delegate = delegate;
this.add_tasks_to_menu_mainloop_id = null;
this.tasks_viewport = [];
this.current_file = this.delegate.get_current_todo_file();
// @key : string (a search query)
// @val : array (of tasks that match the search query)
this.search_dict = new Map();
//
// container
//
this.actor = new St.Bin({ x_fill: true, style_class: 'view-search view-box' });
this.content_box = new St.BoxLayout({ x_expand: true, vertical: true, style_class: 'view-box-content' });
this.actor.add_actor(this.content_box);
//
// search entry
//
{
let box = new St.BoxLayout({ style_class: 'timepp-menu-item' });
this.content_box.add_child(box);
this.search_entry = new St.Entry({ style: `width: ${delegate.settings.get_int('todo-task-width') + 30}px;`, x_expand: true, can_focus: true });
box.add_child(this.search_entry);
box = new St.BoxLayout({ style_class: 'icon-box' });
this.search_entry.set_secondary_icon(box);
this.add_filter_icon = new St.Icon({ visible: false, track_hover: true, reactive: true, gicon : MISC_UTILS.getIcon('timepp-filter-add-symbolic') });
box.add_child(this.add_filter_icon);
this.search_close_icon = new St.Icon({ track_hover: true, reactive: true, style_class: 'close-icon', gicon : MISC_UTILS.getIcon('timepp-close-symbolic') });
box.add_child(this.search_close_icon);
}
//
// task items box
//
this.tasks_scroll = new St.ScrollView({ style_class: 'timepp-menu-item tasks-container vfade search-results', x_fill: true, y_align: St.Align.START});
this.content_box.add(this.tasks_scroll, {expand: true});
this.tasks_scroll.hscrollbar_policy = Gtk.PolicyType.NEVER;
this.tasks_scroll_content = new St.BoxLayout({ vertical: true, style_class: 'tasks-content-box'});
this.tasks_scroll.add_actor(this.tasks_scroll_content);
//
// listen
//
this.search_entry.clutter_text.connect('text-changed', () => this._search());
this.search_close_icon.connect('button-release-event', () => this.delegate.show_view__default());
this.add_filter_icon.connect('button-release-event', () => this._add_custom_filter());
//
// finally
//
this._search();
}
_search () {
if (this.add_tasks_to_menu_mainloop_id) {
Mainloop.source_remove(this.add_tasks_to_menu_mainloop_id);
this.add_tasks_to_menu_mainloop_id = null;
}
this._remove_tasks_from_menu();
let needle = this.search_entry.get_text().trim().toLowerCase();
if (needle === '') {
this.tasks_viewport = this.delegate.tasks;
this.add_filter_icon.visible = false;
this._add_tasks_to_menu();
return;
}
this.add_filter_icon.visible = this.current_file.filters.custom.indexOf(this.search_entry.get_text()) === -1;
let [search_needed, search_space] = this._find_prev_search_results(needle);
if (! search_needed) {
this.tasks_viewport = search_space;
this._add_tasks_to_menu();
return;
}
let reduced_results = [];
for (let i = 0, len = search_space.length; i < len; i++) {
let score = FUZZ.fuzzy_search_v1(needle, search_space[i].task_str.toLowerCase());
if (score !== null) reduced_results.push([i, score]);
}
reduced_results.sort((a, b) => b[1] - a[1]);
this.tasks_viewport = new Array(reduced_results.length);
for (let i = 0; i < reduced_results.length; i++) {
this.tasks_viewport[i] = search_space[ reduced_results[i][0] ];
}
this.search_dict.set(needle, this.tasks_viewport);
this._add_tasks_to_menu();
}
_find_prev_search_results (pattern) {
let res = '';
for (let [old_patt,] of this.search_dict) {
if (pattern.startsWith(old_patt) && old_patt.length > res.length)
res = old_patt;
}
if (pattern === res) return [false, this.search_dict.get(res)];
else if (res) return [true, this.search_dict.get(res)];
else return [true, this.delegate.tasks];
}
_add_tasks_to_menu () {
if (this.add_tasks_to_menu_mainloop_id) {
Mainloop.source_remove(this.add_tasks_to_menu_mainloop_id);
this.add_tasks_to_menu_mainloop_id = null;
}
this.tasks_scroll.vscrollbar_policy = Gtk.PolicyType.NEVER;
this.add_tasks_to_menu_mainloop_id = Mainloop.timeout_add(0, () => {
this._add_tasks_to_menu__finish(0, false);
});
}
_add_tasks_to_menu__finish (i, scrollbar_shown) {
if (!scrollbar_shown && this.ext.needs_scrollbar()) {
this.tasks_scroll.vscrollbar_policy = Gtk.PolicyType.ALWAYS;
scrollbar_shown = true;
}
for (let j = 0; j < 8; j++, i++) {
if (i === this.tasks_viewport.length) {
this.add_tasks_to_menu_mainloop_id = null;
if (!scrollbar_shown && this.ext.needs_scrollbar())
this.tasks_scroll.vscrollbar_policy = Gtk.PolicyType.ALWAYS;
return;
}
let it = this.tasks_viewport[i];
this.tasks_scroll_content.add_child(it.actor);
it.dnd.drag_enabled = false;
it.actor_parent = this.tasks_scroll_content;
it.actor_scrollview = [[this.tasks_scroll], []];
}
this.add_tasks_to_menu_mainloop_id = Mainloop.idle_add(() => {
this._add_tasks_to_menu__finish(i, scrollbar_shown);
});
}
_remove_tasks_from_menu () {
if (this.add_tasks_to_menu_mainloop_id) {
Mainloop.source_remove(this.add_tasks_to_menu_mainloop_id);
this.add_tasks_to_menu_mainloop_id = null;
}
for (let it of this.tasks_viewport) {
it.actor_parent = null;
it.actor_scrollview = null;
}
this.tasks_scroll_content.remove_all_children();
this.tasks_viewport = [];
}
_add_custom_filter () {
let needle = this.search_entry.get_text();
let filters = this.delegate.get_current_todo_file().filters;
if (filters.custom.indexOf(needle) !== -1) return;
filters.custom.push(needle);
filters.custom_active.push(needle);
this.delegate.store_cache();
this.delegate.show_view__default();
}
close () {
this.search_dict.clear();
this._remove_tasks_from_menu();
this.actor.destroy();
}
}
Signals.addSignalMethods(ViewSearch.prototype);

View File

@@ -0,0 +1,230 @@
const St = imports.gi.St;
const Gtk = imports.gi.Gtk;
const Meta = imports.gi.Meta;
const Clutter = imports.gi.Clutter;
const Main = imports.ui.main;
const PopupMenu = imports.ui.popupMenu;
const Signals = imports.signals;
const Mainloop = imports.mainloop;
const ME = imports.misc.extensionUtils.getCurrentExtension();
const Gettext = imports.gettext.domain(ME.metadata['gettext-domain']);
const _ = Gettext.gettext;
const ngettext = Gettext.ngettext;
const DND = ME.imports.lib.dnd;
const MISC = ME.imports.lib.misc_utils;
const G = ME.imports.sections.todo.GLOBAL;
// =====================================================================
// @@@ ViewSort
//
// @ext : obj (main extension object)
// @delegate : obj (main section object)
//
// @signals: 'update-sort'
// =====================================================================
var ViewSort = class ViewSort {
constructor (ext, delegate) {
this.ext = ext;
this.delegate = delegate;
//
// draw
//
this.actor = new St.BoxLayout({ y_expand: true, x_expand: true, style_class: 'view-sort view-box' });
this.content_box = new St.BoxLayout({ y_expand: true, x_expand: true, vertical: true, style_class: 'view-box-content' });
this.actor.add_actor(this.content_box);
this.scrollview = new St.ScrollView({ style_class: 'vfade' });
this.content_box.add_actor(this.scrollview);
this.sort_items_box = new St.BoxLayout({ y_expand: true, x_expand: true, vertical: true, style_class: 'sort-items-box' });
this.scrollview.add_actor(this.sort_items_box);
//
// create sort items
//
{
let sort_text_map = {
[G.SortType.PIN] : _('Sort by Pin'),
[G.SortType.CONTEXT] : _('Sort by Context'),
[G.SortType.PROJECT] : _('Sort by Projects'),
[G.SortType.PRIORITY] : _('Sort by Priority'),
[G.SortType.DUE_DATE] : _('Sort by Due Date'),
[G.SortType.ALPHABET] : _('Sort by Alphabet'),
[G.SortType.RECURRENCE] : _('Sort by Recurrence Date'),
[G.SortType.COMPLETED] : _('Sort by Completed'),
[G.SortType.CREATION_DATE] : _('Sort by Creation Date'),
[G.SortType.COMPLETION_DATE] : _('Sort by Completion Date'),
};
for (let it of this.delegate.get_current_todo_file().sorts) {
let [sort_type, sort_order] = [it[0], it[1]];
let item = new SortItem(delegate, this.scrollview, this.sort_items_box, sort_text_map[sort_type], sort_type, sort_order);
this.sort_items_box.add_child(item.actor);
}
}
{
let sep = new PopupMenu.PopupSeparatorMenuItem();
sep.actor.add_style_class_name('timepp-separator');
this.content_box.add_child(sep.actor);
}
//
// toggle automatic sort
//
this.toggle_automatic_sort = new St.BoxLayout({ x_expand: true, reactive: true, style_class: 'row' });
this.content_box.add_child(this.toggle_automatic_sort);
this.toggle_automatic_sort.add_child(new St.Label({ text: _('Automatic sorting'), x_expand: true, y_align: Clutter.ActorAlign.CENTER }));
this.toggle_automatic_sort_btn = new St.Button({ can_focus: true });
this.toggle_automatic_sort.add_actor(this.toggle_automatic_sort_btn);
this.toggle = new PopupMenu.Switch();
this.toggle_automatic_sort_btn.add_actor(this.toggle.actor);
this.toggle.setToggleState(this.delegate.get_current_todo_file().automatic_sort);
//
// buttons
//
this.btn_box = new St.BoxLayout({ x_expand: true, style_class: 'row btn-box' });
this.content_box.add_child(this.btn_box);
this.button_ok = new St.Button({ can_focus: true, label: _('Ok'), style_class: 'btn-ok button' });
this.btn_box.add(this.button_ok, {expand: true});
//
// listen
//
this.sort_items_box.connect('allocation-changed', () => {
this.scrollview.vscrollbar_policy = Gtk.PolicyType.NEVER;
if (this.ext.needs_scrollbar()) this.scrollview.vscrollbar_policy = Gtk.PolicyType.ALWAYS;
});
this.button_ok.connect('clicked', () => this._on_ok_clicked());
this.toggle_automatic_sort_btn.connect('clicked', () => this._on_toggle_clicked());
this.toggle_automatic_sort.connect('button-press-event', () => this._on_toggle_clicked());
}
_on_ok_clicked () {
let res = [];
for (let it of this.sort_items_box.get_children())
res.push([it._owner.sort_type, it._owner.sort_order]);
this.emit('update-sort', res, this.toggle.state);
}
_on_toggle_clicked () {
this.toggle.setToggleState(!this.toggle.state);
}
close () {
this.actor.destroy();
}
}
Signals.addSignalMethods(ViewSort.prototype);
// =====================================================================
// @@@ SortItem
// =====================================================================
var SortItem = class SortItem{
constructor (delegate, actor_scrollview, actor_parent, label, sort_type, sort_order) {
this.delegate = delegate;
this.actor_scrollview = [[actor_scrollview], []];
this.actor_parent = actor_parent;
this.label = label;
this.sort_type = sort_type;
this.sort_order = sort_order;
//
// draw
//
this.actor = new St.BoxLayout({ x_expand: true, reactive: true, style_class: 'row' });
this.actor._owner = this;
this.label = new St.Label ({ x_expand: true, y_expand: true, text: label, reactive: true, y_align: Clutter.ActorAlign.CENTER });
this.actor.add_child(this.label);
this.icn_box = new St.BoxLayout({ style_class: 'icon-box' });
this.actor.add_actor(this.icn_box);
this.sort_icon = new St.Icon({ reactive: true, can_focus: true, track_hover: true });
this.icn_box.add_actor(this.sort_icon);
this.sort_icon.set_gicon(
MISC.getIcon(
sort_order === G.SortOrder.ASCENDING ?
'timepp-sort-ascending-symbolic' :
'timepp-sort-descending-symbolic'
)
);
//
// DND
//
this.draggable = new DND.Draggable(this);
//
// listen
//
this.label.connect('enter-event', () => {
MISC.global_wrapper.display.set_cursor(Meta.Cursor.MOVE_OR_RESIZE_WINDOW);
});
this.label.connect('leave-event', () => {
MISC.global_wrapper.display.set_cursor(Meta.Cursor.DEFAULT);
});
this.delegate.sigm.connect_press(this.sort_icon, Clutter.BUTTON_PRIMARY, true, () => {
if (this.sort_order === G.SortOrder.ASCENDING) {
this.sort_order = G.SortOrder.DESCENDING;
this.sort_icon.gicon = MISC.getIcon('timepp-sort-descending-symbolic');
} else {
this.sort_order = G.SortOrder.ASCENDING;
this.sort_icon.gicon = MISC.getIcon('timepp-sort-ascending-symbolic');
}
});
this.sort_icon.connect('key-press-event', (_, event) => {
if (event.get_state() !== Clutter.ModifierType.CONTROL_MASK)
return Clutter.EVENT_PROPAGATE;
let i = 0;
let children = this.actor_parent.get_children();
for (; i < children.length; i++) {
if (children[i] === this.actor) break;
}
if (event.get_key_symbol() === Clutter.KEY_Up && i > 0) {
this.actor_parent.set_child_at_index(this.actor, --i);
return Clutter.EVENT_STOP;
} else if (event.get_key_symbol() === Clutter.KEY_Down && i < children.length - 1) {
this.actor_parent.set_child_at_index(this.actor, ++i);
return Clutter.EVENT_STOP;
}
return Clutter.EVENT_PROPAGATE;
});
}
}

View File

@@ -0,0 +1,567 @@
const St = imports.gi.St;
const Gtk = imports.gi.Gtk;
const Gio = imports.gi.Gio
const Meta = imports.gi.Meta;
const Shell = imports.gi.Shell;
const Clutter = imports.gi.Clutter;
const Main = imports.ui.main;
const Signals = imports.signals;
const Mainloop = imports.mainloop;
const ME = imports.misc.extensionUtils.getCurrentExtension();
const Gettext = imports.gettext.domain(ME.metadata['gettext-domain']);
const _ = Gettext.gettext;
const ngettext = Gettext.ngettext;
const TASK = ME.imports.sections.todo.task_item;
const REG = ME.imports.lib.regex;
const FUZZ = ME.imports.lib.fuzzy_search;
const RESIZE = ME.imports.lib.resize;
const MISC_UTILS = ME.imports.lib.misc_utils;
const SIG_MANAGER = ME.imports.lib.signal_manager;
const MULTIL_ENTRY = ME.imports.lib.multiline_entry;
const G = ME.imports.sections.todo.GLOBAL;
const TODO_TXT_SYNTAX_URL = 'https://github.com/zagortenay333/timepp__gnome#todotxt-syntax';
const EditorMode = {
ADD_TASK : "ADD_TASK",
EDIT_TASK : "EDIT_TASK",
};
// =====================================================================
// @@@ ViewTaskEditor
//
// @ext : obj (main extension object)
// @delegate : obj (main section object)
// @task : obj (optional)
//
// @signals:
// - 'add-task' (returns a new task)
// - 'edited-task' (the task has been edited)
// - 'delete-task' (returns bool; if true, the task is to be archived as well)
// - 'cancel'
//
// If @task is provided, then the entry will be prepopulated with the task_str
// of that task object and the signals 'delete-task' and 'edit-task' will be
// used instead of 'add-task'.
// =====================================================================
var ViewTaskEditor = class ViewTaskEditor {
constructor (ext, delegate, task) {
this.ext = ext;
this.delegate = delegate;
Mainloop.idle_add(() => this.delegate.actor.add_style_class_name('view-task-editor'));
this.sigm = new SIG_MANAGER.SignalManager();
this.curr_selected_completion = null;
this.current_word_start = 0;
this.current_word_end = 0;
this.text_changed_handler_block = false;
this.mode = task ? EditorMode.EDIT_TASK : EditorMode.ADD_TASK;
this.old_task_str = "";
//
// container
//
this.actor = new St.BoxLayout({ style_class: 'view-box' });
this.content_box = new St.BoxLayout({ vertical: true, style_class: 'view-box-content' });
this.actor.add_actor(this.content_box);
//
// preview task
//
this.preview_scrollview = new St.ScrollView();
this.actor.add_actor(this.preview_scrollview);
this.preview_scrollview.visible = this.delegate.settings.get_boolean('todo-show-task-editor-preview');
this.preview_scrollbox = new St.BoxLayout({ vertical: true });
this.preview_scrollview.add_actor(this.preview_scrollbox);
if (this.mode === EditorMode.ADD_TASK) {
this.preview_task = new TASK.TaskItem(this.ext, this.delegate, task ? task.task_str : " ", true);
} else {
if (task.actor_parent) task.actor_parent.remove_child(task.actor);
this.preview_task = task;
this.old_task_str = task.task_str;
}
this.preview_task.actor_parent = this.preview_scrollbox;
this.preview_scrollbox.add_child(this.preview_task.actor);
//
// entry
//
this.entry_container = new St.BoxLayout({ vertical: true, style_class: 'row' });
this.content_box.add_child(this.entry_container);
this.entry = new MULTIL_ENTRY.MultiLineEntry(_('Task...'), true);
this.entry_container.add_actor(this.entry.actor);
this.entry.automatic_newline_insert = false;
this.entry.keep_min_height = false;
this.entry.resize_with_keyboard = true;
this.entry.entry.set_size(400, 64);
this.entry.scroll_box.vscrollbar_policy = Gtk.PolicyType.NEVER;
this.entry.scroll_box.hscrollbar_policy = Gtk.PolicyType.NEVER;
if (this.mode === EditorMode.EDIT_TASK)
this.entry.set_text(task.task_str.replace(/\\n/g, '\n'));
this.entry_resize = new RESIZE.MakeResizable(this.entry.entry);
//
// icons
//
let header = new St.BoxLayout();
this.entry_container.insert_child_at_index(header, 0);
{ // help icon
let box = new St.BoxLayout({ style_class: 'icon-box' });
header.add_child(box);
this.help_icon = new St.Icon({ gicon : MISC_UTILS.getIcon('timepp-question-symbolic'), can_focus: true, reactive: true, track_hover: true, x_align: Clutter.ActorAlign.START, y_align: Clutter.ActorAlign.CENTER, });
box.add_child(this.help_icon);
}
{ // other icons
let box = new St.BoxLayout({ x_expand: true, x_align: Clutter.ActorAlign.END, style_class: 'icon-box-group' });
header.add_child(box);
// group 1
let icon_group = new St.BoxLayout({ style_class: 'icon-box' });
box.add_child(icon_group);
this.header_icon = new St.Icon({ gicon : MISC_UTILS.getIcon('timepp-header-symbolic'), can_focus: true, reactive: true, track_hover: true, x_align: Clutter.ActorAlign.START, y_align: Clutter.ActorAlign.CENTER, });
icon_group.add_child(this.header_icon);
this.mark_icon = new St.Icon({ gicon : MISC_UTILS.getIcon('timepp-mark-symbolic'), can_focus: true, reactive: true, track_hover: true, x_align: Clutter.ActorAlign.START, y_align: Clutter.ActorAlign.CENTER, });
icon_group.add_child(this.mark_icon);
// group 2
icon_group = new St.BoxLayout({ style_class: 'icon-box' });
box.add_child(icon_group);
this.bold_icon = new St.Icon({ gicon : MISC_UTILS.getIcon('timepp-bold-symbolic'), can_focus: true, reactive: true, track_hover: true, x_align: Clutter.ActorAlign.START, y_align: Clutter.ActorAlign.CENTER, });
icon_group.add_child(this.bold_icon);
this.italic_icon = new St.Icon({ gicon : MISC_UTILS.getIcon('timepp-italic-symbolic'), can_focus: true, reactive: true, track_hover: true, x_align: Clutter.ActorAlign.START, y_align: Clutter.ActorAlign.CENTER, });
icon_group.add_child(this.italic_icon);
this.strike_icon = new St.Icon({ gicon : MISC_UTILS.getIcon('timepp-strike-symbolic'), can_focus: true, reactive: true, track_hover: true, x_align: Clutter.ActorAlign.START, y_align: Clutter.ActorAlign.CENTER, });
icon_group.add_child(this.strike_icon);
this.underscore_icon = new St.Icon({ gicon : MISC_UTILS.getIcon('timepp-underscore-symbolic'), can_focus: true, reactive: true, track_hover: true, x_align: Clutter.ActorAlign.START, y_align: Clutter.ActorAlign.CENTER, });
icon_group.add_child(this.underscore_icon);
// group 3
icon_group = new St.BoxLayout({ style_class: 'icon-box' });
box.add_child(icon_group);
this.link_icon = new St.Icon({ gicon : MISC_UTILS.getIcon('timepp-link-symbolic'), can_focus: true, reactive: true, track_hover: true, x_align: Clutter.ActorAlign.START, y_align: Clutter.ActorAlign.CENTER, });
icon_group.add_child(this.link_icon);
this.code_icon = new St.Icon({ gicon : MISC_UTILS.getIcon('timepp-code-symbolic'), can_focus: true, reactive: true, track_hover: true, x_align: Clutter.ActorAlign.START, y_align: Clutter.ActorAlign.CENTER, });
icon_group.add_child(this.code_icon);
// group 4
icon_group = new St.BoxLayout({ style_class: 'icon-box' });
box.add_child(icon_group);
this.eye_icon = new St.Icon({ can_focus: true, reactive: true, track_hover: true, x_align: Clutter.ActorAlign.START, y_align: Clutter.ActorAlign.CENTER, });
icon_group.add_child(this.eye_icon);
if (this.preview_scrollview.visible) this.eye_icon.gicon = MISC_UTILS.getIcon('timepp-eye-symbolic');
else this.eye_icon.gicon = MISC_UTILS.getIcon('timepp-eye-closed-symbolic')
}
//
// competion menu
//
this.completion_menu = new St.ScrollView({ hscrollbar_policy: Gtk.PolicyType.NEVER, vscrollbar_policy: Gtk.PolicyType.NEVER, visible: false, style_class: 'vfade' });
this.entry_container.add_child(this.completion_menu);
this.completion_menu_content = new St.BoxLayout({ vertical: true, reactive: true, style_class: 'completion-box' });
this.completion_menu.add_actor(this.completion_menu_content);
//
// buttons
//
this.btn_box = new St.BoxLayout({ style_class: 'row btn-box' });
this.content_box.add_actor(this.btn_box);
if (this.mode === EditorMode.EDIT_TASK) {
this.button_delete = new St.Button({ can_focus: true, label: _('Delete'), style_class: 'btn-delete button', x_expand: true });
this.btn_box.add(this.button_delete, {expand: true});
this.button_delete.connect('clicked', () => this.emit('delete-task'));
}
let current = this.delegate.get_current_todo_file();
if (this.mode === EditorMode.EDIT_TASK && current && current.done_file && !task.hidden) {
this.button_archive = new St.Button({ can_focus: true, label: _('Archive'), style_class: 'btn-delete button', x_expand: true });
this.btn_box.add(this.button_archive, {expand: true});
this.button_archive.connect('clicked', () => this.emit('delete-task', true));
}
this.button_cancel = new St.Button({ can_focus: true, label: _('Cancel'), style_class: 'btn-cancel button', x_expand: true });
this.btn_box.add(this.button_cancel, {expand: true});
this.button_ok = new St.Button({ can_focus: true, label: _('Ok'), style_class: 'btn-ok button', x_expand: true });
this.btn_box.add(this.button_ok, {expand: true});
//
// listen
//
this.preview_task_sid = this.preview_task.actor.connect('captured-event', (_, event) => {
// We can't use the 'captured-event' sig to prevent actors from
// getting focused via the keyboard...
if (event.type() === Clutter.EventType.KEY_RELEASE) this.actor.grab_key_focus();
return Clutter.EVENT_STOP;
});
this.content_box.connect('allocation-changed', () => {
this.entry.scroll_box.vscrollbar_policy = Gtk.PolicyType.NEVER;
this.preview_scrollview.vscrollbar_policy = Gtk.PolicyType.NEVER;
this.preview_scrollview.hscrollbar_policy = Gtk.PolicyType.NEVER;
let [, nat_h] = this.ext.menu.actor.get_preferred_height(-1);
let [, nat_w] = this.ext.menu.actor.get_preferred_width(-1);
let max_h = this.ext.menu_max_h;
let max_w = this.ext.menu_max_w;
if (nat_w >= max_w) this.preview_scrollview.hscrollbar_policy = Gtk.PolicyType.ALWAYS;
if (nat_h >= max_h) {
this.preview_scrollview.vscrollbar_policy = Gtk.PolicyType.ALWAYS;
this.entry.scroll_box.vscrollbar_policy = Gtk.PolicyType.ALWAYS;
}
});
this.entry.entry.connect('key-press-event', (_, event) => {
let symbol = event.get_key_symbol();
if (this.completion_menu.visible && symbol === Clutter.Tab) {
this._on_tab();
return Clutter.EVENT_STOP;
}
});
Mainloop.idle_add(() => { // Connect with a slight delay to avoid some initial confusion.
this.entry.entry.clutter_text.connect('text-changed', () => {
// In idle_add because the cursor_position will not be reported correctly.
Mainloop.idle_add(() => this._on_text_changed());
});
});
this.sigm.connect_release(this.help_icon, Clutter.BUTTON_PRIMARY, true, () => MISC_UTILS.open_web_uri(TODO_TXT_SYNTAX_URL));
this.sigm.connect_release(this.header_icon, Clutter.BUTTON_PRIMARY, true, () => this._insert_markdown('#'));
this.sigm.connect_release(this.mark_icon, Clutter.BUTTON_PRIMARY, true, () => this._insert_markdown('***'));
this.sigm.connect_release(this.bold_icon, Clutter.BUTTON_PRIMARY, true, () => this._insert_markdown('*'));
this.sigm.connect_release(this.italic_icon, Clutter.BUTTON_PRIMARY, true, () => this._insert_markdown('__'));
this.sigm.connect_release(this.strike_icon, Clutter.BUTTON_PRIMARY, true, () => this._insert_markdown('~~'));
this.sigm.connect_release(this.underscore_icon, Clutter.BUTTON_PRIMARY, true, () => this._insert_markdown('___'));
this.sigm.connect_release(this.link_icon, Clutter.BUTTON_PRIMARY, true, () => this._find_file());
this.sigm.connect_release(this.code_icon, Clutter.BUTTON_PRIMARY, true, () => this._insert_markdown('``'));
this.sigm.connect_release(this.eye_icon, Clutter.BUTTON_PRIMARY, true, () => this._toggle_preview());
this.entry.entry.clutter_text.connect('activate', () => this._on_activate());
this.button_ok.connect('clicked', () => this._emit_ok());
this.button_cancel.connect('clicked', () => this._emit_cancel());
this.actor.connect('key-press-event', (_, event) => {
switch (event.get_key_symbol()) {
case Clutter.KEY_KP_Enter:
case Clutter.Return:
if (event.get_state() === Clutter.ModifierType.CONTROL_MASK) this._emit_ok();
break;
case Clutter.KEY_f:
if (event.get_state() === Clutter.ModifierType.CONTROL_MASK) this._find_file();
break;
}
});
}
_on_text_changed () {
if (this.text_changed_handler_block) return Clutter.EVENT_PROPAGATE;
let text = this.entry.entry.get_text();
this.preview_task.reset(true, text || " ", false)
let [word, start, end] = this._get_current_word();
if (word && /[@+]/.test(word[0])) {
this._show_completions(word);
this.current_word_start = start;
this.current_word_end = end;
} else {
this.completion_menu.hide();
}
}
_on_tab () {
this.curr_selected_completion.pseudo_class = '';
let next = this.curr_selected_completion.get_next_sibling();
if (next) {
this.curr_selected_completion = next;
next.pseudo_class = 'active';
} else {
this.curr_selected_completion = this.completion_menu_content.first_child;
this.curr_selected_completion.pseudo_class = 'active';
}
MISC_UTILS.scroll_to_item(this.completion_menu, this.completion_menu_content, this.curr_selected_completion);
}
_on_activate () {
if (!this.completion_menu.visible || !this.curr_selected_completion) {
this.entry.insert_text('\n');
return;
}
this.text_changed_handler_block = true;
let completion = this.curr_selected_completion.label;
this.entry.entry.text =
this.entry.entry.get_text().slice(0, this.current_word_start) +
completion + ' ' +
this.entry.entry.get_text().slice(this.current_word_end + 1);
let text = this.entry.entry.get_text();
this.preview_task.reset(true, text || " ", false)
// @BUG or feature?
// Setting the cursor pos directly seeems to also select the text, so
// use set_selection instead.
let p = this.current_word_start + completion.length + 1;
this.entry.entry.clutter_text.set_selection(p, p);
this.curr_selected_completion = null;
this.completion_menu.hide();
this.text_changed_handler_block = false;
}
_on_completion_hovered (item) {
// It seems that when the completion menu gets hidden, the items are
// moving for a brief moment which triggers the hover callback.
// We prevent any possible issues in this case by just checking whether
// the menu is visible.
if (! this.completion_menu.visible) return;
this.curr_selected_completion.pseudo_class = '';
this.curr_selected_completion = item;
item.pseudo_class = 'active';
}
_emit_cancel () {
if (this.mode === EditorMode.EDIT_TASK)
this.preview_task.reset(true, this.old_task_str, false)
this.emit('cancel');
}
_emit_ok () {
if (this.done) return;
let text = this._create_task_str();
if (! text) return;
this.done = true;
let r = this.preview_task;
this.preview_task.actor.disconnect(this.preview_task_sid);
this.preview_scrollbox.remove_child(this.preview_task.actor);
this.preview_task.actor_parent = null;
this.preview_task = null;
r.task_str = text;
if (this.mode === EditorMode.ADD_TASK) this.emit('add-task', r);
else this.emit('edited-task');
}
_insert_markdown (delim) {
let text = this.entry.entry.get_text();
let pos = this.entry.entry.clutter_text.get_cursor_position();
let bound = this.entry.entry.clutter_text.get_selection_bound();
if (pos === -1) pos = text.length;
if (bound === -1) bound = text.length;
let word;
let end;
let start;
if (bound === pos) { // nothing selected so wrap current word
[word, start, end] = this._get_current_word();
} else {
word = this.entry.entry.clutter_text.get_selection() + "";
if (bound < pos) {
start = bound;
end = pos;
} else {
start = pos;
end = bound;
}
if (end > 0) end--;
}
this.entry.entry.text = text.slice(0, start) + delim + word + delim + text.slice(end + 1);
let l = delim.length;
if (bound === pos) this.entry.entry.clutter_text.set_selection(pos + l, pos + l);
else this.entry.entry.clutter_text.set_selection(start + l, end + l + 1);
}
_toggle_preview () {
let state = !this.delegate.settings.get_boolean('todo-show-task-editor-preview');
if (state) this.eye_icon.gicon = MISC_UTILS.getIcon('timepp-eye-symbolic');
else this.eye_icon.gicon = MISC_UTILS.getIcon('timepp-eye-closed-symbolic')
this.preview_scrollview.visible = state;
this.delegate.settings.set_boolean('todo-show-task-editor-preview', state);
}
_find_file () {
this.ext.menu.close();
this.file_chooser = MISC_UTILS.open_file_dialog(false, (out) => {
if (out) this.entry.insert_text(out);
this.todo_file_chooser_proc = null;
this.ext.menu.open();
Mainloop.idle_add(() => this.entry.entry.grab_key_focus());
});
}
// @word: string (a context or project)
_show_completions (word) {
let completions = null;
if (word === '(')
completions = this._find_completions(word, this.delegate.stats.priorities);
else if (word[0] === '@')
completions = this._find_completions(word, this.delegate.stats.contexts);
else if (word[0] === '+')
completions = this._find_completions(word, this.delegate.stats.projects);
if (!completions || completions.length === 0) {
this.completion_menu.hide();
return;
}
this.completion_menu_content.destroy_all_children();
this.completion_menu.show();
for (let i = 0; i < completions.length; i++) {
let item = new St.Button({ label: completions[i], reactive: true, track_hover: true, x_align: St.Align.START, style_class: 'row popup-menu-item' });
this.completion_menu_content.add_child(item);
item.connect('notify::hover', (item) => this._on_completion_hovered(item));
item.connect('clicked', (item) => this._on_completion_selected());
}
this.completion_menu_content.first_child.pseudo_class = 'active';
this.curr_selected_completion = this.completion_menu_content.first_child;
}
// @needle : string (a context or project)
// @haystack : map (of all contexts or projects);
//
// If @needle is a context, then the @haystack has to be the map of all
// contexts. Likewise for projects.
_find_completions (needle, haystack) {
if (needle === '@' || needle === '+') {
let res = [];
for (let [key,] of haystack) res.push(key);
return res;
}
let reduced_results = [];
let score;
for (let [keyword,] of haystack) {
score = FUZZ.fuzzy_search_v1(needle, keyword);
if (!score) continue;
reduced_results.push([score, keyword]);
}
reduced_results.sort((a, b) => a[0] < b[0]);
let results = [];
for (let i = 0, len = reduced_results.length; i < len; i++) {
results[i] = reduced_results[i][1];
}
return results;
}
_get_current_word () {
let text = this.entry.entry.get_text();
let len = text.length;
let pos = this.entry.entry.clutter_text.get_cursor_position();
if (pos === -1) pos = len;
let start = pos - 1;
let end = pos;
while (start > -1 && !/\s/.test(text[start])) start--;
while (end < len && !/\s/.test(text[end])) end++;
start++;
if (end > 0) end--;
return [text.substring(start, end + 1), start, end];
}
_create_task_str () {
let text = this.entry.entry.get_text();
if (! text) return "";
let words = text.split(' ');
if (this.mode === EditorMode.EDIT_TASK) return text.replace(/\n/g, '\\n');
// If in add mode, we insert a creation date if the user didn't do it.
if (words[0] === 'x') {
if (!Date.parse(words[1]))
words.splice(1, 0, MISC_UTILS.date_yyyymmdd(), MISC_UTILS.date_yyyymmdd());
else if (words[2] && !Date.parse(words[2]))
words.splice(2, 0, MISC_UTILS.date_yyyymmdd());
}
else if (REG.TODO_PRIO.test(words[0])) {
if (words[1] && !Date.parse(words[1]))
words.splice(1, 0, MISC_UTILS.date_yyyymmdd());
}
else if (!Date.parse(words[0])) {
words.splice(0, 0, MISC_UTILS.date_yyyymmdd());
}
return words.join(' ').replace(/\n/g, '\\n');
}
close () {
if (this.file_chooser_proc) this.file_chooser_proc.force_exit();
if (this.preview_task) {
this.preview_task.actor.disconnect(this.preview_task_sid);
this.preview_scrollbox.remove_child(this.preview_task.actor);
this.preview_task.actor_parent = null;
this.preview_task = null;
}
Mainloop.timeout_add(0, () => {
this.actor.destroy();
this.delegate.actor.remove_style_class_name('view-task-editor');
});
}
}
Signals.addSignalMethods(ViewTaskEditor.prototype);

View File

@@ -0,0 +1,105 @@
const Gtk = imports.gi.Gtk;
const Clutter = imports.gi.Clutter;
const Main = imports.ui.main;
const Signals = imports.signals;
const Mainloop = imports.mainloop;
const ME = imports.misc.extensionUtils.getCurrentExtension();
const MISC_UTILS = ME.imports.lib.misc_utils;
const G = ME.imports.sections.todo.GLOBAL;
// =====================================================================
// @@@ View Manager
//
// @ext : obj (main extension object)
// @delegate : obj (main section object)
//
// - The todo section is always in a particular view.
// - A view must be enlisted in the View enum.
// - To switch to a new view, use the show_view function of this object.
// - The current_view is always stored in the current_view var of this obj.
// =====================================================================
var ViewManager = class ViewManager {
constructor (ext, delegate) {
this.ext = ext;
this.delegate = delegate;
this.lock = false;
this.container = this.delegate.actor;
this.reset();
}
reset () {
this.current_view = null;
this.current_view_name = "";
this.actors = [];
this.open_callback = null;
this.close_callback = null;
this.show_tasks_mainloop_id = null;
}
close_current_view () {
if (typeof this.close_callback === 'function') this.close_callback();
this.reset();
}
// @view_params: object of the form: { view : object
// view_name : View
// actors : array
// focused_actors : object
// close_callback : func
// open_callback : func }
//
// @view:
// The main object of the view. Can be used by the main view to call some
// methods on it.
//
// @view_name:
//
//
// @actors (can be omitted if @open_callback is given):
// Array of all the top-level actors that need to be in the popup
// menu. These are the actors that make up the particular view.
//
// @focused_actor:
// Actor that will be put into focus when the view is shown.
//
// @close_callback:
// Function that is used to close this view when another view needs
// to be shown.
//
// @open_callback (optional):
// Function that is used to open the view. If it is not given, then
// opening the view means that the actors will be added to the popup menu.
show_view (view_params) {
if (typeof this.close_callback === 'function') this.close_callback();
this.current_view = view_params.view || null;
this.current_view_name = view_params.view_name;
this.actors = view_params.actors;
this.close_callback = view_params.close_callback;
this.open_callback = view_params.open_callback || null;
if (typeof this.open_callback === 'function') {
this.open_callback();
} else {
this.container.remove_all_children();
// @HACK: Seems to speed things up when written like this...
for (let actor of this.actors) actor.hide();
for (let actor of this.actors) this.container.add_actor(actor);
for (let actor of this.actors) actor.show();
}
if (view_params.focused_actor && this.ext.menu.isOpen) {
view_params.focused_actor.grab_key_focus();
}
}
}
Signals.addSignalMethods(ViewManager.prototype);