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 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 MISC_UTILS = ME.imports.lib.misc_utils; const FULLSCREEN = ME.imports.lib.fullscreen; const SIG_MANAGER = ME.imports.lib.signal_manager; const KEY_MANAGER = ME.imports.lib.keybinding_manager; const PANEL_ITEM = ME.imports.lib.panel_item; const IFACE = `${ME.path}/dbus/stopwatch_iface.xml`; const CACHE_FILE = '~/.cache/timepp_gnome_shell_extension/timepp_stopwatch.json'; const StopwatchState = { RUNNING : 'RUNNING', STOPPED : 'STOPPED', RESET : 'RESET', }; const ClockFormat = { H_M : 0, H_M_S : 1, H_M_S_CS : 2, }; const NotifStyle = { STANDARD : 0, FULLSCREEN : 1, }; const PanelMode = { ICON : 0, TEXT : 1, ICON_TEXT : 2, DYNAMIC : 3, }; // ===================================================================== // @@@ Main // // @ext : obj (main extension object) // @settings : obj (extension settings) // ===================================================================== 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('stopwatch-section'); this.separate_menu = this.settings.get_boolean('stopwatch-separate-menu'); this.clock_format = this.settings.get_enum('stopwatch-clock-format'); this.start_time = 0; // for computing elapsed time (microseconds) this.cache_file = null; this.cache = null; this.tic_mainloop_id = null; this.time_backup_mainloop_id = null; this.stopwatch_state = StopwatchState.RESET; { 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/Stopwatch'); } this.fullscreen = new StopwatchFullscreen(this.ext, this, this.settings.get_int('stopwatch-fullscreen-monitor-pos')); this.sigm = new SIG_MANAGER.SignalManager(); this.keym = new KEY_MANAGER.KeybindingManager(this.settings); try { this.cache_file = MISC_UTILS.file_new_for_path(CACHE_FILE); let cache_format_version = ME.metadata['cache-file-format-version'].stopwatch; if (this.cache_file.query_exists(null)) { let [a, contents, b] = 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, time : 0, // microseconds laps : [], }; } } catch (e) { logError(e); return; } // // keybindings // this.keym.add('stopwatch-keybinding-open', () => { this.ext.open_menu(this.section_name); }); this.keym.add('stopwatch-keybinding-open-fullscreen', () => { this.show_fullscreen(); }); // // panel item // this.panel_item.icon.gicon = MISC_UTILS.getIcon('timepp-stopwatch-symbolic'); this.panel_item.actor.add_style_class_name('stopwatch-panel-item'); this._toggle_panel_mode(); switch (this.clock_format) { case ClockFormat.H_M: this.panel_item.set_label('00:00'); this.fullscreen.set_banner_text('00:00') break; case ClockFormat.H_M_S: this.panel_item.set_label('00:00:00'); this.fullscreen.set_banner_text('00:00:00') break; case ClockFormat.H_M_S_CS: this.panel_item.set_label('00:00:00.00'); this.fullscreen.set_banner_text('00:00:00.00') } // // header // this.header = new St.BoxLayout({ style_class: 'timepp-menu-item header' }); this.actor.add_actor(this.header); this.header_label = new St.Label({ x_expand: true, text: _('Stopwatch'), style_class: 'clock' }); this.header.add_child(this.header_label); this.icon_box = new St.BoxLayout({ y_align: Clutter.ActorAlign.CENTER, x_align: Clutter.ActorAlign.END, style_class: 'icon-box' }); this.header.add_child(this.icon_box); this.fullscreen_icon = new St.Icon({ reactive: true, can_focus: true, track_hover: true, gicon : MISC_UTILS.getIcon('timepp-fullscreen-symbolic'), style_class: 'fullscreen-icon' }); this.icon_box.add_actor(this.fullscreen_icon); // // buttons // this.stopwatch_button_box = new St.BoxLayout({ x_expand: true, style_class: 'timepp-menu-item btn-box' }); this.actor.add_child(this.stopwatch_button_box); this.button_reset = new St.Button({ can_focus: true, label: _('Reset'), style_class: 'btn-reset button', x_expand: true, visible: false }); this.button_lap = new St.Button({ can_focus: true, label: _('Lap'), style_class: 'btn-lap button', x_expand: true, visible: false }); this.button_start = new St.Button({ can_focus: true, label: _('Start'), style_class: 'btn-start button', x_expand: true }); this.button_stop = new St.Button({ can_focus: true, label: _('Stop'), style_class: 'btn-stop button', x_expand: true, visible: false }); this.stopwatch_button_box.add_child(this.button_reset); this.stopwatch_button_box.add_child(this.button_lap); this.stopwatch_button_box.add_child(this.button_start); this.stopwatch_button_box.add_child(this.button_stop); // // laps box // this.laps_scroll = new St.ScrollView({ visible: false, style_class: 'timepp-menu-item laps-scrollview vfade', x_fill: true, y_fill: false, y_align: St.Align.START}); this.actor.add_actor(this.laps_scroll); this.laps_scroll.vscrollbar_policy = Gtk.PolicyType.NEVER; this.laps_scroll.hscrollbar_policy = Gtk.PolicyType.NEVER; this.laps_scroll_bin = new St.BoxLayout({ vertical: true, style_class: 'laps-box' }); this.laps_scroll.add_actor(this.laps_scroll_bin); this.laps_string = new St.Label(); this.laps_scroll_bin.add_actor(this.laps_string); // // listen // this.sigm.connect(this.fullscreen, 'monitor-changed', () => { this.settings.set_int('stopwatch-fullscreen-monitor-pos', this.fullscreen.monitor); }); this.sigm.connect(this.settings, 'changed::stopwatch-separate-menu', () => { this.separate_menu = this.settings.get_boolean('stopwatch-separate-menu'); this.ext.update_panel_items(); }); this.sigm.connect(this.settings, 'changed::stopwatch-clock-format', () => { this.clock_format = this.settings.get_enum('stopwatch-clock-format'); let txt = this._time_format_str(); this.panel_item.set_label(txt); this.fullscreen.set_banner_text(txt); }); this.sigm.connect(this.laps_string, 'allocation-changed', () => { this.laps_scroll.vscrollbar_policy = Gtk.PolicyType.NEVER; if (ext.needs_scrollbar()) this.laps_scroll.vscrollbar_policy = Gtk.PolicyType.ALWAYS; }); this.sigm.connect(this.settings, 'changed::stopwatch-panel-mode', () => this._toggle_panel_mode()); this.sigm.connect(this.panel_item, 'middle-click', () => this.stopwatch_toggle()); this.sigm.connect_release(this.fullscreen_icon, Clutter.BUTTON_PRIMARY, true, () => this.show_fullscreen()); this.sigm.connect_release(this.button_start, Clutter.BUTTON_PRIMARY, true, () => this.start()); this.sigm.connect_release(this.button_reset, Clutter.BUTTON_PRIMARY, true, () => this.reset()); this.sigm.connect_release(this.button_stop, Clutter.BUTTON_PRIMARY, true, () => this.stop()); this.sigm.connect_release(this.button_lap, Clutter.BUTTON_PRIMARY, true, () => this.lap()); // // finally // if (this.cache.time > 0) { this._update_laps(); this._update_time_display(); this.stopwatch_state = StopwatchState.STOPPED; } } disable_section () { if (this.time_backup_mainloop_id) { Mainloop.source_remove(this.time_backup_mainloop_id); this.time_backup_mainloop_id = null; } if (this.stopwatch_state === StopwatchState.RUNNING) this.stop(); this.dbus_impl.unexport(); this._store_cache(); this.sigm.clear(); this.keym.clear(); if (this.fullscreen) { this.fullscreen.destroy(); this.fullscreen = null; } super.disable_section(); } _store_cache () { 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); } start () { this.start_time = GLib.get_monotonic_time() - this.cache.time; this.stopwatch_state = StopwatchState.RUNNING; if (!this.fullscreen.is_open && this.actor.visible) this.button_stop.grab_key_focus(); this._toggle_buttons(); this._panel_item_UI_update(); this.fullscreen.on_timer_started(); if (this.settings.get_enum('stopwatch-panel-mode') === PanelMode.DYNAMIC) this.panel_item.set_mode('icon_text'); if (! this.time_backup_mainloop_id) this._periodic_time_backup(); this._store_cache(); this._tic(); } stop () { if (this.start_time) this.cache.time = GLib.get_monotonic_time() - this.start_time; this.stopwatch_state = StopwatchState.STOPPED; if (this.tic_mainloop_id) { Mainloop.source_remove(this.tic_mainloop_id); this.tic_mainloop_id = null; } if (this.time_backup_mainloop_id) { Mainloop.source_remove(this.time_backup_mainloop_id); this.time_backup_mainloop_id = null; } this.fullscreen.on_timer_stopped(); if (!this.fullscreen.is_open && this.actor.visible) this.button_start.grab_key_focus(); this._panel_item_UI_update(); this._toggle_buttons(); if (this.settings.get_enum('stopwatch-panel-mode') === PanelMode.DYNAMIC) this.panel_item.set_mode('icon'); this._store_cache(); } reset () { this.stopwatch_state = StopwatchState.RESET; if (this.tic_mainloop_id) { Mainloop.source_remove(this.tic_mainloop_id); this.tic_mainloop_id = null; } if (this.time_backup_mainloop_id) { Mainloop.source_remove(this.time_backup_mainloop_id); this.time_backup_mainloop_id = null; } this.fullscreen.on_timer_reset(); if (!this.fullscreen.is_open && this.actor.visible) this.button_start.grab_key_focus(); this.cache.laps = []; this.cache.time = 0; this._destroy_laps(); this._update_time_display(); this._toggle_buttons(); this._panel_item_UI_update(); this.header_label.text = _('Stopwatch'); if (this.settings.get_enum('stopwatch-panel-mode') === PanelMode.DYNAMIC) this.panel_item.set_mode('icon'); this._store_cache(); } stopwatch_toggle () { if (this.stopwatch_state === StopwatchState.RUNNING) this.stop(); else this.start(); } _tic () { this.cache.time = GLib.get_monotonic_time() - this.start_time; this._update_time_display(); if (this.clock_format === ClockFormat.H_M_S_CS) { this.tic_mainloop_id = Mainloop.timeout_add(10, () => this._tic()); } else { this.tic_mainloop_id = Mainloop.timeout_add_seconds(1, () => this._tic()); } } _update_time_display () { let txt = this._time_format_str(); this.header_label.text = txt; this.panel_item.set_label(txt); this.fullscreen.set_banner_text(txt); } _time_format_str () { let t = Math.floor(this.cache.time / 10000); // centiseconds let cs = t % 100; t = Math.floor(t / 100); let h = Math.floor(t / 3600); t = t % 3600; let m = Math.floor(t / 60); let s = t % 60; switch (this.clock_format) { case ClockFormat.H_M: return "%02d:%02d".format(h, m); case ClockFormat.H_M_S: return "%02d:%02d:%02d".format(h, m, s); case ClockFormat.H_M_S_CS: return "%02d:%02d:%02d.%02d".format(h, m, s, cs); } } _panel_item_UI_update () { if (this.stopwatch_state === StopwatchState.RUNNING) this.panel_item.actor.add_style_class_name('on'); else this.panel_item.actor.remove_style_class_name('on'); } lap () { if (this.stopwatch_state !== StopwatchState.RUNNING) return; this.cache.laps.push(this._time_format_str()); this._store_cache(); this._update_laps(); } _update_laps () { let n = this.cache.laps.length; if (n === 0) return; let pad = String(n).length + 1; let markup = ''; while (n--) { markup += `${n + 1} ` + Array(pad - String(n + 1).length).join(' ') + `- ${this.cache.laps[n]}\n`; } markup = `${markup.slice(0, -1)}`; this.laps_string.clutter_text.set_markup(markup); this.fullscreen.laps_string.clutter_text.set_markup(markup); this.laps_scroll.show(); this.fullscreen.laps_scroll.show(); } _destroy_laps () { this.laps_scroll.hide(); this.laps_string.text = ''; } _toggle_buttons () { switch (this.stopwatch_state) { case StopwatchState.RESET: this.button_reset.hide(); this.button_lap.hide(); this.button_start.show(); this.button_stop.hide(); this.button_start.add_style_pseudo_class('first-child'); this.button_start.add_style_pseudo_class('last-child'); this.fullscreen.button_reset.hide(); this.fullscreen.button_lap.hide(); this.fullscreen.button_start.show(); this.fullscreen.button_stop.hide(); this.fullscreen.button_start.add_style_pseudo_class('first-child'); this.fullscreen.button_start.add_style_pseudo_class('last-child'); break; case StopwatchState.RUNNING: this.button_reset.show(); this.button_lap.show(); this.button_start.hide(); this.button_stop.show(); this.fullscreen.button_reset.show(); this.fullscreen.button_lap.show(); this.fullscreen.button_start.hide(); this.fullscreen.button_stop.show(); break; case StopwatchState.STOPPED: this.button_reset.show(); this.button_lap.hide(); this.button_start.show(); this.button_stop.hide(); this.button_start.remove_style_pseudo_class('first-child'); this.button_start.add_style_pseudo_class('last-child'); this.fullscreen.button_reset.show(); this.fullscreen.button_lap.hide(); this.fullscreen.button_start.show(); this.fullscreen.button_stop.hide(); this.fullscreen.button_start.remove_style_pseudo_class('first-child'); this.fullscreen.button_start.add_style_pseudo_class('last-child'); } } show_fullscreen () { this.ext.menu.close(); if (! this.fullscreen) { this.fullscreen = new StopwatchFullscreen(this.ext, this, this.settings.get_int('stopwatch-fullscreen-monitor-pos')); } this.fullscreen.open(); } _periodic_time_backup () { this.cache.time = GLib.get_monotonic_time() - this.start_time; this._store_cache(); this.time_backup_mainloop_id = Mainloop.timeout_add_seconds(60, () => { this._periodic_time_backup(); }); } _toggle_panel_mode () { switch (this.settings.get_enum('stopwatch-panel-mode')) { case PanelMode.ICON: this.panel_item.set_mode('icon'); break; case PanelMode.TEXT: this.panel_item.set_mode('text'); break; case PanelMode.ICON_TEXT: this.panel_item.set_mode('icon_text'); break; case PanelMode.DYNAMIC: if (this.stopwatch_state === StopwatchState.RUNNING) this.panel_item.set_mode('icon_text'); else this.panel_item.set_mode('icon'); } } // returns int (microseconds) get_time () { if (this.stopwatch_state === StopwatchState.RUNNING) return GLib.get_monotonic_time() - this.start_time; else return this.cache.time; } } Signals.addSignalMethods(SectionMain.prototype); // ===================================================================== // @@@ Stopwatch fullscreen interface // // @ext : obj (main extension object) // @delegate : obj (main section object) // @monitor : int // // signals: 'monitor-changed' // ===================================================================== var StopwatchFullscreen = class StopwatchFullscreen extends FULLSCREEN.Fullscreen{ constructor (ext, delegate, monitor) { super(monitor); this.middle_box.vertical = false; this.ext = ext; this.delegate = delegate; this.default_style_class = this.actor.style_class; // // laps box // this.laps_scroll = new St.ScrollView({ visible: false, x_fill: true, y_fill: true, y_align: St.Align.START, style_class: 'laps-scrollview vfade' }); this.middle_box.add_child(this.laps_scroll); this.laps_scroll.hscrollbar_policy = Gtk.PolicyType.NEVER; this.laps_scroll_bin = new St.BoxLayout({ vertical: true, style_class: 'laps-box' }); this.laps_scroll.add_actor(this.laps_scroll_bin); this.laps_string = new St.Label(); this.laps_scroll_bin.add_actor(this.laps_string); // // buttons // this.stopwatch_button_box = new St.BoxLayout({ x_expand: true, y_expand: true, style_class: 'btn-box', x_align: Clutter.ActorAlign.CENTER, y_align: Clutter.ActorAlign.CENTER, }); this.bottom_box.add_child(this.stopwatch_button_box); this.button_reset = new St.Button({ can_focus: true, label: _('Reset'), style_class: 'btn-reset button', visible: false }); this.button_lap = new St.Button({ can_focus: true, label: _('Lap'), style_class: 'btn-lap button', visible: false }); this.button_stop = new St.Button({ can_focus: true, label: _('Stop'), style_class: 'btn-stop button', visible: false }); this.button_start = new St.Button({ can_focus: true, label: _('Start'), style_class: 'btn-start button' }); this.stopwatch_button_box.add_child(this.button_reset); this.stopwatch_button_box.add_child(this.button_lap); this.stopwatch_button_box.add_child(this.button_start); this.stopwatch_button_box.add_child(this.button_stop); // // listen // this.button_start.connect('clicked', () => { this.delegate.start(); return Clutter.EVENT_STOP; }); this.button_reset.connect('clicked', () => { this.delegate.reset(); return Clutter.EVENT_STOP; }); this.button_stop.connect('clicked', () => { this.delegate.stop(); return Clutter.EVENT_STOP; }); this.button_lap.connect('clicked', () => { this.delegate.lap(); return Clutter.EVENT_STOP; }); this.actor.connect('key-release-event', (_, event) => { switch (event.get_key_symbol()) { case Clutter.KEY_space: this.delegate.stopwatch_toggle(); return Clutter.EVENT_STOP; case Clutter.KEY_l: case Clutter.KEY_KP_Enter: case Clutter.Return: this.delegate.lap(); return Clutter.EVENT_STOP; case Clutter.KEY_r: case Clutter.KEY_BackSpace: this.delegate.reset(); return Clutter.EVENT_STOP; default: return Clutter.EVENT_PROPAGATE; } }); } close () { if (this.delegate.stopwatch_state === StopwatchState.RESET) { this.actor.style_class = this.default_style_class; switch (this.delegate.clock_format) { case ClockFormat.H_M: this.set_banner_text('00:00') break; case ClockFormat.H_M_S: this.set_banner_text('00:00:00') break; case ClockFormat.H_M_S_CS: this.set_banner_text('00:00:00.00') break; } } super.close(); } on_timer_started () { this.actor.style_class = this.default_style_class; } on_timer_stopped () { this.actor.style_class = this.default_style_class + ' timer-stopped'; } on_timer_reset () { this.laps_scroll.hide(); this.laps_string.text = ''; } } Signals.addSignalMethods(StopwatchFullscreen.prototype);