Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions packages/background-tips/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,21 @@
Displays tips about Pulsar in the background when there are no open editors.

![Screen shot](https://f.cloud.github.com/assets/69169/1796267/c3de038c-6a60-11e3-8bf8-36f45684902c.png)

### Contributing tips

Packages can contribute tips by adding a `backgroundTips` array to their `package.json`. Each entry is a string displayed as-is, with optional `{command}` placeholders that are replaced by the current keybinding for that command.

```json
"backgroundTips": [
"You can toggle the Tree View with {atom-workspace>tree-view:toggle}",
"You can open any file quickly using {atom-workspace>fuzzy-finder:toggle-file-finder}"
]
```

The placeholder syntax is `{command}` or `{scope>command}`:

- `{command}` resolves the keybinding using the current platform.
- `{scope>command}` resolves the keybinding for the given CSS selector scope (e.g. `atom-workspace`, `atom-text-editor`, `body`).

If the command in a placeholder has no keybinding defined, the tip is skipped entirely.
245 changes: 138 additions & 107 deletions packages/background-tips/lib/background-tips-view.js
Original file line number Diff line number Diff line change
@@ -1,156 +1,187 @@
const _ = require('underscore-plus')
const {CompositeDisposable, Disposable} = require('atom')
const Tips = require('./tips')
const _ = require('underscore-plus');
const { CompositeDisposable } = require('atom');

const TEMPLATE = `\
<ul class="centered background-message">
<li class="message"></li>
</ul>\
`

module.exports =
class BackgroundTipsElement {
constructor () {
this.element = document.createElement('background-tips')
this.index = -1
this.workspaceCenter = atom.workspace.getCenter()

this.startDelay = 1000
this.displayDuration = 10000
this.fadeDuration = 300

this.disposables = new CompositeDisposable()

const visibilityCallback = () => this.updateVisibility()

this.disposables.add(this.workspaceCenter.onDidAddPane(visibilityCallback))
this.disposables.add(this.workspaceCenter.onDidDestroyPane(visibilityCallback))
this.disposables.add(this.workspaceCenter.onDidChangeActivePaneItem(visibilityCallback))

atom.getCurrentWindow().on('blur', visibilityCallback)
atom.getCurrentWindow().on('focus', visibilityCallback)

this.disposables.add(new Disposable(() => atom.getCurrentWindow().removeListener('blur', visibilityCallback)))
this.disposables.add(new Disposable(() => atom.getCurrentWindow().removeListener('focus', visibilityCallback)))

this.startTimeout = setTimeout(() => this.start(), this.startDelay)
</ul>`;

module.exports = class BackgroundTipsElement {
constructor() {
this.element = document.createElement('background-tips');
this.index = -1;
this.workspaceCenter = atom.workspace.getCenter();
this.startDelay = 1000;
this.fadeDuration = 300;
this.tips = [];
this.tipSources = [];
this.started = false;
this.disposables = new CompositeDisposable();
const visibilityCallback = () => this.updateVisibility();
this.disposables.add(
this.workspaceCenter.onDidAddPane(visibilityCallback),
this.workspaceCenter.onDidDestroyPane(visibilityCallback),
this.workspaceCenter.onDidChangeActivePaneItem(visibilityCallback),
atom.config.observe('background-tips.displayDuration', (value) => {
this.displayDuration = value * 1000;
}),
);
this.startTimeout = setTimeout(() => { this.started = true; this.start(); }, this.startDelay);
}

destroy () {
this.stop()
this.disposables.dispose()
destroy() {
this.stop();
this.disposables.dispose();
}

attach () {
this.element.innerHTML = TEMPLATE
this.message = this.element.querySelector('.message')

const paneView = atom.views.getView(this.workspaceCenter.getActivePane())
const itemViews = paneView.querySelector('.item-views')
let top = 0
attach() {
this.element.innerHTML = TEMPLATE;
this.message = this.element.querySelector('.message');
const paneView = atom.views.getView(this.workspaceCenter.getActivePane());
const itemViews = paneView.querySelector('.item-views');
let top = 0;
if (itemViews && itemViews.offsetTop) {
top = itemViews.offsetTop
top = itemViews.offsetTop;
}

this.element.style.top = top + 'px'
paneView.appendChild(this.element)
this.element.style.top = top + 'px';
paneView.appendChild(this.element);
}

detach () {
this.element.remove()
}

updateVisibility () {
updateVisibility() {
if (this.shouldBeAttached()) {
this.start()
this.start();
} else {
this.stop()
this.stop();
}
}

shouldBeAttached () {
return this.workspaceCenter.getPanes().length === 1 &&
this.workspaceCenter.getActivePaneItem() == null &&
atom.getCurrentWindow().isFocused()
shouldBeAttached() {
return (
this.workspaceCenter.getPanes().length === 1 &&
this.workspaceCenter.getActivePaneItem() == null
);
}

start () {
if (!this.shouldBeAttached() || this.interval != null) return
this.renderTips()
this.randomizeIndex()
this.attach()
this.showNextTip()
this.interval = setInterval(() => this.showNextTip(), this.displayDuration)
start() {
if (!this.shouldBeAttached() || this.interval != null) return;
if (this.tips.length === 0) return;
this.randomizeIndex();
this.attach();
this.showNextTip();
this.interval = setInterval(() => this.showNextTip(), this.displayDuration);
}

stop () {
this.element.remove()
stop() {
this.element.remove();
if (this.interval != null) {
clearInterval(this.interval)
clearInterval(this.interval);
}

clearTimeout(this.startTimeout)
clearTimeout(this.nextTipTimeout)
this.interval = null
clearTimeout(this.startTimeout);
clearTimeout(this.nextTipTimeout);
this.interval = null;
}

randomizeIndex () {
const len = Tips.length
this.index = Math.round(Math.random() * len) % len
randomizeIndex() {
const len = this.tips.length;
this.index = len > 0 ? Math.round(Math.random() * len) % len : 0;
}

showNextTip () {
this.index = ++this.index % Tips.length
this.message.classList.remove('fade-in')
showNextTip() {
if (this.tips.length === 0) return;
let html = null;
for (let i = 0; i < this.tips.length; i++) {
this.index = (this.index + 1) % this.tips.length;
if (atom.packages.isPackageDisabled(this.tipSources[this.index])) continue;
html = this.renderTip(this.tips[this.index]);
if (html !== null) break;
this.log(`Skipping tip (missing keybinding): "${this.tips[this.index]}"`);
}
if (html === null) {
this.stop();
return;
}
this.message.classList.remove('fade-in');
this.nextTipTimeout = setTimeout(() => {
this.message.innerHTML = Tips[this.index]
this.message.classList.add('fade-in')
}, this.fadeDuration)
this.log(`Displaying tip [${this.index + 1}/${this.tips.length}]: "${this.tips[this.index]}"`);
this.message.innerHTML = html;
this.message.classList.add('fade-in');
}, this.fadeDuration);
}

renderTips () {
if (this.tipsRendered) return
for (let i = 0; i < Tips.length; i++) {
const tip = Tips[i]
Tips[i] = this.renderTip(tip)
addPackageTips(pkg) {
const raw = pkg.metadata.backgroundTips;
if (!Array.isArray(raw) || raw.length === 0) return;
for (const tip of raw) {
this.tips.push(tip);
this.tipSources.push(pkg.name);
}
this.tipsRendered = true
this.log(`Package "${pkg.name}" added ${raw.length} tip(s), total: ${this.tips.length}`);
if (this.started && this.interval == null) this.start();
}

renderTip (str) {
str = str.replace(/\{(.+)\}/g, (match, command) => {
let binding, scope
const scopeAndCommand = command.split('>')
if (scopeAndCommand.length > 1) {
[scope, command] = scopeAndCommand
removePackageTips(pkg) {
const keep = [];
const keepSources = [];
for (let i = 0; i < this.tips.length; i++) {
if (this.tipSources[i] !== pkg.name) {
keep.push(this.tips[i]);
keepSources.push(this.tipSources[i]);
}
const bindings = atom.keymaps.findKeyBindings({command: command.trim()})
}
const removed = this.tips.length - keep.length;
if (removed === 0) return;
this.log(`Package "${pkg.name}" removed ${removed} tip(s), total: ${keep.length}`);
this.tips = keep;
this.tipSources = keepSources;
if (this.interval != null) {
if (this.tips.length === 0) {
this.stop();
} else {
this.index = Math.min(this.index, this.tips.length - 1);
}
}
}

renderTip(str) {
let missing = false;
const html = str.replace(/\{(.+)\}/g, (match, command) => {
let binding, scope;
const scopeAndCommand = command.split('>');
if (scopeAndCommand.length > 1) {
[scope, command] = scopeAndCommand;
}
const bindings = atom.keymaps.findKeyBindings({ command: command.trim() });
if (scope) {
for (binding of bindings) {
if (binding.selector === scope) break
if (binding.selector === scope) break;
}
} else {
binding = this.getKeyBindingForCurrentPlatform(bindings)
binding = this.getKeyBindingForCurrentPlatform(bindings);
}

if (binding && binding.keystrokes) {
const keystrokeLabel = _.humanizeKeystroke(binding.keystrokes).replace(/\s+/g, '&nbsp')
return `<span class="keystroke">${keystrokeLabel}</span>`
const keystrokeLabel = _.humanizeKeystroke(binding.keystrokes).replace(/\s+/g, '&nbsp');
return `<span class="keystroke">${keystrokeLabel}</span>`;
} else {
return command
missing = true;
return '';
}
})
return str
});
return missing ? null : html;
}

getKeyBindingForCurrentPlatform (bindings) {
if (!bindings || !bindings.length) return
for (let binding of bindings) {
getKeyBindingForCurrentPlatform(bindings) {
if (!bindings || !bindings.length) return;
for (const binding of bindings) {
if (binding.selector.indexOf(process.platform) !== -1) {
return binding
return binding;
}
}
return bindings[0]
return bindings[0];
}

log(...args) {
if (atom.config.get('background-tips.debug')) {
console.log('[background-tips]', ...args);
}
}
}
};
28 changes: 21 additions & 7 deletions packages/background-tips/lib/background-tips.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
const BackgroundTipsView = require('./background-tips-view')
const { CompositeDisposable } = require("atom");
const BackgroundTipsView = require("./background-tips-view");

module.exports = {
activate () {
this.backgroundTipsView = new BackgroundTipsView()
activate() {
this.backgroundTipsView = new BackgroundTipsView();
this.disposables = new CompositeDisposable();
for (let pkg of atom.packages.getLoadedPackages()) {
this.backgroundTipsView.addPackageTips(pkg);
}
this.disposables.add(
atom.packages.onDidLoadPackage((pkg) => {
this.backgroundTipsView.addPackageTips(pkg);
}),
atom.packages.onDidUnloadPackage((pkg) => {
this.backgroundTipsView.removePackageTips(pkg);
}),
)
},

deactivate () {
this.backgroundTipsView.destroy()
}
}
deactivate() {
this.disposables.dispose();
this.backgroundTipsView.destroy();
},
};
14 changes: 0 additions & 14 deletions packages/background-tips/lib/tips.js

This file was deleted.

21 changes: 21 additions & 0 deletions packages/background-tips/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,27 @@
"engines": {
"atom": ">0.42.0"
},
"backgroundTips": [
"You can split your editor into multiple panes with {pane:split-right-and-copy-active-item}",
"You can move a line of text up with {atom-text-editor>editor:move-line-up}",
"You can move a line of text down with {atom-text-editor>editor:move-line-down}",
"You can toggle line comments with {atom-text-editor>editor:toggle-line-comments}"
],
"configSchema": {
"displayDuration": {
"type": "integer",
"default": 10,
"minimum": 1,
"description": "How long each tip is displayed, in seconds.",
"order": 1
},
"debug": {
"type": "boolean",
"default": false,
"description": "Log debug information about tip collection and display to the console.",
"order": 2
}
},
"dependencies": {
"underscore-plus": "1.x"
}
Expand Down
Loading
Loading