Commit 0a119add authored by Dylan McCall's avatar Dylan McCall

Added a central list of enabled breaks, instead of keeping track of state in...

Added a central list of enabled breaks, instead of keeping track of state in each break's own settings.
Tweaked break settings dialog to use a combo box to choose the configuration, instead of an on/off switch beside each break. (Fixes multiple ways to disable all breaks).
Added a message in the main window if the break helper application is not running when it should be. (This currently appears more often than it should because the settings application is not autostarting the helper application).
parent d2724d62
......@@ -5,20 +5,25 @@
<schema id="org.brainbreak.breaks" path="/org/brainbreak/breaks/" gettext-domain="brainbreak">
<key name="master-enabled" type="b">
<default>true</default>
<_summary>True to enable Brain Break</_summary>
<default>true</default>
</key>
<key name="selected-breaks" type="as">
<_summary>The list of breaks that are currently enabled</_summary>
<default>[]</default>
</key>
<key name="quiet-mode" type="b">
<default>false</default>
<_summary>True to enable quiet mode</_summary>
<_description>Hides breaks until quiet-mode-expire-time.</_description>
<default>false</default>
</key>
<key name="quiet-mode-expire-time" type="x">
<default>0</default>
<_summary>Time to end quiet mode.</_summary>
<_description>The time to automatically disable quiet mode, in unix time.</_description>
<default>0</default>
</key>
<child schema="org.brainbreak.breaks.restbreak" name="restbreak"/>
......@@ -26,34 +31,24 @@
</schema>
<schema id="org.brainbreak.breaks.restbreak" path="/org/brainbreak/breaks/restbreak/" gettext-domain="brainbreak">
<key name="enabled" type="b">
<default>true</default>
<_summary>True to enable break type</_summary>
</key>
<key name="interval-seconds" type="i">
<default>2400</default>
<_summary>Time between breaks</_summary>
<_description>The time between rest breaks, in seconds.</_description>
<default>2400</default>
</key>
<key name="duration-seconds" type="i">
<default>360</default>
<_summary>Duration of each break</_summary>
<_description>The duration of each rest break, in seconds.</_description>
<default>360</default>
</key>
</schema>
<schema id="org.brainbreak.breaks.microbreak" path="/org/brainbreak/breaks/microbreak/" gettext-domain="brainbreak">
<key name="enabled" type="b">
<default>true</default>
<_summary>True to enable break type</_summary>
</key>
<key name="interval-seconds" type="i">
<default>360</default>
<_summary>Time between breaks</_summary>
<_description>The preferred time between micro breaks, in seconds.</_description>
<default>360</default>
</key>
<key name="duration-seconds" type="i">
......
......@@ -19,14 +19,24 @@ public class BreakManager : Object {
private Application application;
private UIManager ui_manager;
private Gee.Map<string, BreakType> breaks;
private BreakHelperServer break_helper_server;
private Gee.Map<string, BreakType> breaks;
private Settings settings;
public bool master_enabled {get; set;}
public string[] selected_break_ids {get; set;}
public BreakManager(Application application, UIManager ui_manager) {
this.application = application;
this.ui_manager = ui_manager;
this.breaks = new Gee.HashMap<string, BreakType>();
this.settings = new Settings("org.brainbreak.breaks");
this.settings.bind("master-enabled", this, "master-enabled", SettingsBindFlags.DEFAULT);
this.settings.bind("selected-breaks", this, "selected-break-ids", SettingsBindFlags.DEFAULT);
this.notify["master-enabled"].connect(this.update_enabled_breaks);
this.notify["selected-break-ids"].connect(this.update_enabled_breaks);
this.break_helper_server = new BreakHelperServer(this);
try {
......@@ -54,6 +64,8 @@ public class BreakManager : Object {
this.add_break(new MicroBreakType(activity_monitor));
this.add_break(new RestBreakType(activity_monitor));
}
this.update_enabled_breaks();
}
public Gee.Set<string> all_break_ids() {
......@@ -71,10 +83,13 @@ public class BreakManager : Object {
private void add_break(BreakType break_type) {
this.breaks.set(break_type.id, break_type);
break_type.initialize(this.ui_manager);
// At the moment, we expect breaks to enable and disable themselves
// using settings keys under their own namespaces. In the future, we
// might want a global list of enabled break types, instead.
}
private void update_enabled_breaks() {
foreach (BreakType break_type in this.all_breaks()) {
bool is_enabled = this.master_enabled && break_type.id in this.selected_break_ids;
break_type.break_controller.set_enabled(is_enabled);
}
}
}
......
......@@ -83,10 +83,10 @@ public abstract class BreakController : Object {
* @param enable True to enable the break, false to disable it
*/
public void set_enabled(bool enable) {
if (enable && ! this.is_enabled()) {
if (enable && !this.is_enabled()) {
this.state = State.WAITING;
this.enabled();
} else if (this.is_enabled()) {
} else if (!enable && this.is_enabled()) {
this.state = State.DISABLED;
this.finished(BreakController.FinishedReason.DISABLED);
this.disabled();
......
......@@ -21,27 +21,15 @@ public abstract class BreakType : Object {
public BreakView break_view;
protected Settings settings;
protected Settings global_settings;
public BreakType(string id, Settings settings) {
this.id = id;
this.settings = settings;
this.global_settings = new Settings("org.brainbreak.breaks");
}
public virtual void initialize(UIManager ui_manager) {
this.break_controller = this.get_break_controller(this.settings);
this.break_view = this.get_break_view(this.break_controller, ui_manager);
this.global_settings.changed["master-enabled"].connect(this.update_enabled);
this.settings.changed["enabled"].connect(this.update_enabled);
this.update_enabled();
}
private void update_enabled() {
bool is_enabled = this.global_settings.get_boolean("master-enabled") &&
this.settings.get_boolean("enabled");
this.break_controller.set_enabled(is_enabled);
}
protected abstract BreakController get_break_controller(Settings settings);
......
......@@ -95,8 +95,10 @@ public abstract class TimerBreakController : BreakController {
}
private void finished_cb(BreakController.FinishedReason reason) {
this.interval_countdown.reset();
this.duration_countdown.reset();
if (reason > BreakController.FinishedReason.DISABLED) {
this.interval_countdown.reset();
this.duration_countdown.reset();
}
}
bool is_warned;
......
......@@ -26,10 +26,18 @@ public class BreakManager : Object {
private Gee.Map<string, BreakType> breaks;
private List<BreakType> breaks_ordered;
private Settings settings;
public bool master_enabled {get; set;}
public string[] selected_break_ids {get; set;}
public BreakManager(Application application) {
this.application = application;
this.breaks = new Gee.HashMap<string, BreakType>();
this.breaks_ordered = new List<BreakType>();
this.settings = new Settings("org.brainbreak.breaks");
this.settings.bind("master-enabled", this, "master-enabled", SettingsBindFlags.DEFAULT);
this.settings.bind("selected-breaks", this, "selected-break-ids", SettingsBindFlags.DEFAULT);
}
public signal void break_added(BreakType break_type);
......@@ -71,8 +79,8 @@ public class BreakManager : Object {
}
}
private void break_status_changed(BreakType break_type, BreakStatus break_status) {
if (break_status.is_focused && break_status.is_active) {
private void break_status_changed(BreakType break_type, BreakStatus? break_status) {
if (break_status != null && break_status.is_focused && break_status.is_active) {
this.set_foreground_break(break_type);
} else if (this.foreground_break == break_type) {
this.set_foreground_break(null);
......
/*
* This file is part of Brain Break.
*
* Brain Break is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Brain Break is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Brain Break. If not, see <http://www.gnu.org/licenses/>.
*/
public class BreakSettingsDialog : Gtk.Dialog {
private BreakManager break_manager;
private ApplicationPanel application_panel;
private BreakConfigurationChooser configuration_chooser;
private Gtk.Grid breaks_grid;
private static const int ABOUT_BUTTON_RESPONSE = 5;
public BreakSettingsDialog(BreakManager break_manager) {
Object();
this.break_manager = break_manager;
Settings settings = new Settings("org.brainbreak.breaks");
this.set_title(_("Choose Your Break Schedule"));
this.set_resizable(false);
this.delete_event.connect(this.hide_on_delete);
Gtk.Widget close_button = this.add_button(Gtk.Stock.CLOSE, Gtk.ResponseType.CLOSE);
this.response.connect(this.response_cb);
Gtk.Container content_area = (Gtk.Container)this.get_content_area();
Gtk.Grid content = new Gtk.Grid();
content_area.add(content);
content.set_orientation(Gtk.Orientation.VERTICAL);
content.set_margin_left(10);
content.set_margin_right(10);
this.configuration_chooser = new BreakConfigurationChooser();
content.add(this.configuration_chooser);
this.configuration_chooser.add_configuration(
{"microbreak", "restbreak"},
_("A mix of short breaks and long breaks")
);
this.configuration_chooser.add_configuration(
{"restbreak"},
_("Occasional long breaks")
);
this.configuration_chooser.add_configuration(
{"microbreak"},
_("Frequent short breaks")
);
settings.bind("selected-breaks", this.configuration_chooser, "selected-break-ids", SettingsBindFlags.DEFAULT);
this.breaks_grid = new Gtk.Grid();
content.add(this.breaks_grid);
this.breaks_grid.set_orientation(Gtk.Orientation.VERTICAL);
content.show_all();
break_manager.break_added.connect(this.break_added_cb);
this.configuration_chooser.notify["selected-break-ids"].connect(this.update_break_configuration);
}
private void update_break_configuration() {
foreach (BreakType break_type in this.break_manager.all_breaks()) {
if (break_type.id in this.configuration_chooser.selected_break_ids) {
break_type.settings_panel.show();
} else {
break_type.settings_panel.hide();
}
}
}
private void break_added_cb(BreakType break_type) {
var settings_panel = break_type.settings_panel;
breaks_grid.add(settings_panel);
settings_panel.set_margin_top(10);
settings_panel.set_margin_bottom(10);
this.update_break_configuration();
}
private void response_cb(int response_id) {
if (response_id == Gtk.ResponseType.CLOSE) {
this.hide();
}
}
}
class BreakConfigurationChooser : Gtk.ComboBox {
public class Configuration : Object {
public Gtk.TreeIter iter;
public string[] break_ids;
public string label;
public Configuration(string[] break_ids, string label) {
this.break_ids = break_ids;
this.label = label;
}
public bool matches_breaks(string[] test_break_ids) {
if (test_break_ids.length == this.break_ids.length) {
foreach (string test_break_id in test_break_ids) {
if (! (test_break_id in this.break_ids)) return false;
}
return true;
} else {
return false;
}
}
}
private Gtk.ListStore list_store;
private List<Configuration> configurations;
public string[] selected_break_ids {public get; public set;}
public BreakConfigurationChooser() {
Object();
this.configurations = new List<Configuration>();
this.list_store = new Gtk.ListStore(2, typeof(Configuration), typeof(string));
this.set_model(this.list_store);
var label_renderer = new Gtk.CellRendererText();
this.pack_start(label_renderer, true);
this.add_attribute(label_renderer, "text", 1);
this.notify["active"].connect(this.send_selected_break);
this.notify["selected-break-ids"].connect(this.receive_selected_break);
}
public void add_configuration(string[] break_ids, string label) {
var configuration = new Configuration(break_ids, label);
this.configurations.append(configuration);
Gtk.TreeIter iter;
this.list_store.append(out iter);
this.list_store.set(iter, 0, configuration, 1, configuration.label);
configuration.iter = iter;
}
private void send_selected_break() {
Gtk.TreeIter iter;
if (this.get_active_iter(out iter)) {
Value value;
this.list_store.get_value(iter, 0, out value);
Configuration configuration = (Configuration)value;
this.selected_break_ids = configuration.break_ids;
}
}
private void receive_selected_break() {
var configuration = this.get_configuration_for_break_ids(this.selected_break_ids);
if (configuration != null) {
this.set_active_iter(configuration.iter);
} else {
this.set_active(-1);
}
}
private Configuration? get_configuration_for_break_ids(string[] selected_breaks) {
foreach (Configuration configuration in this.configurations) {
if (configuration.matches_breaks(selected_breaks)) {
return configuration;
}
}
return null;
}
}
......@@ -17,8 +17,7 @@
public abstract class BreakType : Object {
public string id {get; private set;}
public bool enabled {get; protected set;}
public BreakStatus status;
public BreakStatus? status;
public BreakInfoPanel info_panel;
public BreakStatusPanel status_panel;
......@@ -29,11 +28,9 @@ public abstract class BreakType : Object {
public BreakType(string id, Settings settings) {
this.id = id;
this.settings = settings;
settings.bind("enabled", this, "enabled", SettingsBindFlags.DEFAULT);
}
public signal void status_changed(BreakStatus status);
public signal void status_changed(BreakStatus? status);
public virtual void initialize() {
this.info_panel = this.get_info_panel();
......@@ -41,7 +38,7 @@ public abstract class BreakType : Object {
this.settings_panel = this.get_settings_panel();
}
protected void update_status(BreakStatus status) {
protected void update_status(BreakStatus? status) {
this.status = status;
this.status_changed(status);
}
......@@ -102,9 +99,19 @@ public abstract class BreakStatusPanel : Gtk.Grid {
}
}
public abstract class BreakSettingsPanel : SettingsPanel {
public abstract class BreakSettingsPanel : Gtk.Grid {
private Gtk.Grid header;
private Gtk.Grid details;
public BreakSettingsPanel(BreakType break_type, string title, string? description) {
base();
Object();
this.set_orientation(Gtk.Orientation.VERTICAL);
this.set_row_spacing(10);
this.header = new Gtk.Grid();
this.add(this.header);
this.header.set_column_spacing(12);
var title_grid = new Gtk.Grid();
this.set_header(title_grid);
......@@ -112,28 +119,40 @@ public abstract class BreakSettingsPanel : SettingsPanel {
title_grid.set_row_spacing(4);
var title_label = new Gtk.Label(title);
title_label.set_halign(Gtk.Align.START);
title_label.get_style_context().add_class("_settings-title");
title_grid.add(title_label);
title_label.get_style_context().add_class("_settings-title");
title_label.set_halign(Gtk.Align.FILL);
title_label.set_hexpand(true);
title_label.set_justify(Gtk.Justification.CENTER);
var description_label = new Gtk.Label("<small>%s</small>".printf(description));
description_label.set_use_markup(true);
description_label.set_halign(Gtk.Align.START);
description_label.get_style_context().add_class("_settings-description");
title_grid.add(description_label);
var toggle_switch = new Gtk.Switch();
this.set_header_action(toggle_switch);
toggle_switch.set_hexpand(true);
toggle_switch.set_halign(Gtk.Align.END);
toggle_switch.set_valign(Gtk.Align.CENTER);
break_type.settings.bind("enabled", toggle_switch, "active", SettingsBindFlags.DEFAULT);
// var description_label = new Gtk.Label("<small>%s</small>".printf(description));
// title_grid.add(description_label);
// description_label.get_style_context().add_class("_settings-description");
// description_label.set_use_markup(true);
// description_label.set_halign(Gtk.Align.FILL);
// description_label.set_hexpand(true);
// description_label.set_justify(Gtk.Justification.CENTER);
this.details = new Gtk.Grid();
this.add(this.details);
this.details.set_margin_left(12);
this.details.set_halign(Gtk.Align.CENTER);
this.details.set_hexpand(true);
this.show_all();
toggle_switch.notify["active"].connect((s, p) => {
bool enabled = toggle_switch.active;
this.set_editable(enabled);
});
}
protected void set_header(Gtk.Widget content) {
this.header.attach(content, 0, 0, 1, 1);
}
protected void set_header_action(Gtk.Widget content) {
this.header.attach(content, 1, 0, 1, 1);
content.set_halign(Gtk.Align.END);
content.set_valign(Gtk.Align.CENTER);
}
protected void set_details(Gtk.Widget content) {
this.details.add(content);
}
}
......@@ -21,7 +21,7 @@ public class MainWindow : Gtk.ApplicationWindow {
private IBreakHelper? break_helper_server;
private Gd.HeaderBar header;
private SettingsDialog settings_dialog;
private BreakSettingsDialog break_settings_dialog;
private Gd.Stack main_stack;
private StatusPanel status_panel;
......@@ -34,9 +34,9 @@ public class MainWindow : Gtk.ApplicationWindow {
this.set_title(_("Break Timer"));
this.set_hide_titlebar_when_maximized(true);
this.settings_dialog = new SettingsDialog(break_manager);
this.settings_dialog.set_modal(true);
this.settings_dialog.set_transient_for(this);
this.break_settings_dialog = new BreakSettingsDialog(break_manager);
this.break_settings_dialog.set_modal(true);
this.break_settings_dialog.set_transient_for(this);
Gtk.Grid content = new Gtk.Grid();
this.add(content);
......@@ -104,7 +104,7 @@ public class MainWindow : Gtk.ApplicationWindow {
}
private void settings_clicked_cb() {
this.settings_dialog.show();
this.break_settings_dialog.show();
}
private void launch_helper() {
......@@ -119,11 +119,16 @@ public class MainWindow : Gtk.ApplicationWindow {
}
}
private class WelcomePanel : Gd.Stack {
}
private class StatusPanel : Gd.Stack {
private BreakManager break_manager;
private Gtk.Grid breaks_list;
private Gtk.Grid no_breaks_message;
private Gtk.Grid error_message;
public StatusPanel(BreakManager break_manager) {
// TODO: Once we port to Gtk.Stack, set property "homogenous: false"
......@@ -141,6 +146,9 @@ private class StatusPanel : Gd.Stack {
this.no_breaks_message = this.build_no_breaks_message();
this.add(this.no_breaks_message);
this.error_message = this.build_error_message();
this.add(this.error_message);
break_manager.break_added.connect(this.break_added_cb);
}
......@@ -153,27 +161,45 @@ private class StatusPanel : Gd.Stack {
return breaks_list;
}
private Gtk.Grid build_message(string icon_name, string heading, string detail) {
var message = new Gtk.Grid();
message.set_orientation(Gtk.Orientation.VERTICAL);
message.set_halign(Gtk.Align.CENTER);
message.set_valign(Gtk.Align.CENTER);
message.set_row_spacing(12);
var image = new Gtk.Image.from_icon_name(icon_name, Gtk.IconSize.DIALOG);
message.add(image);
image.set_pixel_size(120);
image.get_style_context().add_class("_break-status-icon");
var heading_label = new Gtk.Label(heading);
message.add(heading_label);
heading_label.get_style_context().add_class("_break-status-heading");
var detail_label = new Gtk.Label(null);
message.add(detail_label);
detail_label.set_markup(detail);
detail_label.get_style_context().add_class("_break-status-hint");
detail_label.set_max_width_chars(60);
return message;
}
private Gtk.Grid build_no_breaks_message() {
var no_breaks_message = new Gtk.Grid();
no_breaks_message.set_orientation(Gtk.Orientation.VERTICAL);
no_breaks_message.set_halign(Gtk.Align.CENTER);
no_breaks_message.set_valign(Gtk.Align.CENTER);
no_breaks_message.set_row_spacing(12);
var no_breaks_image = new Gtk.Image.from_icon_name("face-sick-symbolic", Gtk.IconSize.DIALOG);
no_breaks_message.add(no_breaks_image);
no_breaks_image.set_pixel_size(120);
no_breaks_image.get_style_context().add_class("_break-status-icon");
var no_breaks_heading = new Gtk.Label(_("Break Timer is taking a break"));
no_breaks_message.add(no_breaks_heading);
no_breaks_heading.get_style_context().add_class("_break-status-heading");
var no_breaks_detail = new Gtk.Label(_("Turn me on to get those breaks going"));
no_breaks_message.add(no_breaks_detail);
no_breaks_detail.get_style_context().add_class("_break-status-hint");
return no_breaks_message;
return this.build_message(
"face-sad-symbolic",
_("Break Timer is taking a break"),
_("Turn me on to get those breaks going")
);
}
private Gtk.Grid build_error_message() {
return this.build_message(
"face-sick-symbolic",
_("Break Timer isn’t responding"),
_("If this continues the next time you log in, please <a href=\"https://bugs.launchpad.net/brainbreak\">open a bug report</a>.")
);
}
private void break_added_cb(BreakType break_type) {
......@@ -189,21 +215,30 @@ private class StatusPanel : Gd.Stack {
}
private void update_breaks_list() {
bool success = false;
bool any_breaks_enabled = false;
foreach (BreakType break_type in this.break_manager.all_breaks()) {
if (break_type.status.is_enabled) {
break_type.status_panel.show();
any_breaks_enabled = true;
} else {
break_type.status_panel.hide();
unowned List<BreakType> all_breaks = this.break_manager.all_breaks();
foreach (BreakType break_type in all_breaks) {
var status = break_type.status;
if (status != null) {
success = true;
if (status.is_enabled) {
break_type.status_panel.show();
any_breaks_enabled = true;
} else {
break_type.status_panel.hide();
}
}
}
if (all_breaks.length() == 0) success = true;
if (any_breaks_enabled) {
this.set_visible_child(this.breaks_list);
} else {
} else if (success || !this.break_manager.master_enabled) {
this.set_visible_child(this.no_breaks_message);
} else {
this.set_visible_child(this.error_message);
}
}
}
......
......@@ -11,12 +11,11 @@ bin_PROGRAMS = \
brainbreak_settings_SOURCES = \
ApplicationPanel.vala \
BreakManager.vala \
BreakSettingsDialog.vala \
BreakType.vala \
MainWindow.vala \
MicroBreakType.vala \
RestBreakType.vala \
SettingsDialog.vala \
SettingsPanel.vala \
TimeChooser.vala \
TimerBreakType.vala \
main.vala
......
/*
* This file is part of Brain Break.
*
* Brain Break is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Brain Break is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Brain Break. If not, see <http://www.gnu.org/licenses/>.
*/
public class SettingsDialog : Gtk.Dialog {
private ApplicationPanel application_panel;
private Gtk.Grid breaks_grid;
private static const int ABOUT_BUTTON_RESPONSE = 5;
public SettingsDialog(BreakManager break_manager) {
Object();
this.set_title(_("Choose Your Break Preferences"));
this.set_resizable(false);
this.delete_event.connect(this.hide_on_delete);
Gtk.Widget close_button = this.add_button(Gtk.Stock.CLOSE, Gtk.ResponseType.CLOSE);
this.response.connect(this.response_cb);
Gtk.Box content = (Gtk.Box) this.get_content_area();
this.breaks_grid = new Gtk.Grid();
this.breaks_grid.set_orientation(Gtk.Orientation.VERTICAL);
this.breaks_grid.margin = 12;
this.breaks_grid.set_row_spacing(18);
content.add(this.breaks_grid);
content.show_all();
break_manager.break_added.connect(this.break_added_cb);
}
private void break_added_cb(BreakType break_type) {
breaks_grid.add(break_type.settings_panel);
}
private void response_cb(int response_id) {
if (response_id == Gtk.ResponseType.CLOSE) {