Skip to content

Commit

Permalink
Use built-in busy spinner instead of changing text
Browse files Browse the repository at this point in the history
  • Loading branch information
TwitchBronBron committed Oct 31, 2023
1 parent ec7258d commit 3a96ae6
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 87 deletions.
24 changes: 14 additions & 10 deletions src/ActiveDeviceManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,27 +39,31 @@ export class ActiveDeviceManager {
this.deviceCache = new NodeCache({ stdTTL: 3600, checkperiod: 120 });
//anytime a device leaves the cache (either expired or manually deleted)
this.deviceCache.on('del', (deviceId, device) => {
void this.emit('device-expire', device);
void this.emit('device-expired', device);
});
this.processEnabledState();
}

private emitter = new EventEmitter();

public on(eventName: 'device-expire', handler: (device: RokuDeviceDetails) => void): Disposable;
public on(eventName: 'device-found', handler: (device: RokuDeviceDetails) => void): Disposable;
public on(eventName: string, handler: (payload: any) => void): Disposable {
public on(eventName: 'device-expired', handler: (device: RokuDeviceDetails) => void, disposables?: Disposable[]): () => void;
public on(eventName: 'device-found', handler: (device: RokuDeviceDetails) => void, disposables?: Disposable[]): () => void;
public on(eventName: string, handler: (payload: any) => void, disposables?: Disposable[]): () => void {
this.emitter.on(eventName, handler);
return {
dispose: () => {
if (this.emitter !== undefined) {
this.emitter.removeListener(eventName, handler);
}
const unsubscribe = () => {
if (this.emitter !== undefined) {
this.emitter.removeListener(eventName, handler);
}
};

disposables?.push({
dispose: unsubscribe
});

return unsubscribe;
}

private async emit(eventName: 'device-expire', device: RokuDeviceDetails);
private async emit(eventName: 'device-expired', device: RokuDeviceDetails);
private async emit(eventName: 'device-found', device: RokuDeviceDetails);
private async emit(eventName: string, data?: any) {
//emit these events on next tick, otherwise they will be processed immediately which could cause issues
Expand Down
21 changes: 10 additions & 11 deletions src/DebugConfigurationProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ describe('BrightScriptConfigurationProvider', () => {

it('includes "manual', () => {
expect(
configProvider['createHostQuickPickList']([], undefined, '')
configProvider['createHostQuickPickList']([], undefined)
).to.eql([{
label: 'Enter manually',
device: {
Expand All @@ -378,7 +378,7 @@ describe('BrightScriptConfigurationProvider', () => {

it('includes separators for devices and manual options', () => {
expect(
configProvider['createHostQuickPickList']([devices[0]], undefined, '')
configProvider['createHostQuickPickList']([devices[0]], undefined)
).to.eql([
{
kind: QuickPickItemKind.Separator,
Expand All @@ -402,7 +402,7 @@ describe('BrightScriptConfigurationProvider', () => {

it('moves active device to the top', () => {
expect(
configProvider['createHostQuickPickList']([devices[0], devices[1], devices[2]], devices[1], '').map(x => x.label)
configProvider['createHostQuickPickList']([devices[0], devices[1], devices[2]], devices[1]).map(x => x.label)
).to.eql([
'last used',
label(devices[1]),
Expand All @@ -416,47 +416,46 @@ describe('BrightScriptConfigurationProvider', () => {

it('includes the spinner text when "last used" and "other devices" separators are both present', () => {
expect(
configProvider['createHostQuickPickList'](devices, devices[1], ' (searching ...)').map(x => x.label)
configProvider['createHostQuickPickList'](devices, devices[1]).map(x => x.label)
).to.eql([
'last used',
label(devices[1]),
'other devices',
label(devices[0]),
label(devices[2]),
'(searching ...)',
' ',
'Enter manually'
]);
});

it('includes the spinner text if "devices" separator is present', () => {
expect(
configProvider['createHostQuickPickList'](devices, null, ' (searching ...)').map(x => x.label)
configProvider['createHostQuickPickList'](devices, null).map(x => x.label)
).to.eql([
'devices',
label(devices[0]),
label(devices[1]),
label(devices[2]),
'(searching ...)',
' ',
'Enter manually'
]);
});

it('includes the spinner text if only "last used" separator is present', () => {
expect(
configProvider['createHostQuickPickList']([devices[0]], devices[0], ' (searching ...)').map(x => x.label)
configProvider['createHostQuickPickList']([devices[0]], devices[0]).map(x => x.label)
).to.eql([
'last used',
label(devices[0]),
'(searching ...)',
' ',
'Enter manually'
]);
});

it('includes the spinner text when no other device entries are present', () => {
expect(
configProvider['createHostQuickPickList']([], null, ' (searching ...)').map(x => x.label)
configProvider['createHostQuickPickList']([], null).map(x => x.label)
).to.eql([
'(searching ...)',
'Enter manually'
]);
});
Expand Down
125 changes: 60 additions & 65 deletions src/DebugConfigurationProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,35 +400,22 @@ export class BrightScriptDebugConfigurationProvider implements DebugConfiguratio
return config;
}

private async promptForHostManual() {
return this.openInputBox('The IP address of your Roku device');
}

/**
* Validates the host parameter in the config and opens an input ui if set to ${promptForHost}
* @param config current config object
*/
private async processHostParameter(config: BrightScriptLaunchConfiguration): Promise<BrightScriptLaunchConfiguration> {
let showInputBox = false;

if (config.host.trim() === '${promptForHost}' || (config?.deepLinkUrl?.includes('${promptForHost}'))) {
if (this.activeDeviceManager.enabled) {
const host = await this.promptForHost();
if (host === 'Enter manually') {
showInputBox = true;
} else if (host) {
config.host = host;
} else {
// User canceled. Give them one more change to enter an ip
showInputBox = true;
}
config.host = await this.promptForHost();
} else {
showInputBox = true;
config.host = await this.promptForHostManual();
}
} else {
showInputBox = true;
}

if (showInputBox) {
config.host = await this.openInputBox('The IP address of your Roku device');
}
// #endregion

//check the host and throw error if not provided or update the workspace to set last host
if (!config.host) {
Expand All @@ -444,7 +431,7 @@ export class BrightScriptDebugConfigurationProvider implements DebugConfiguratio
* Prompt the user to pick a host from a list of devices
*/
private async promptForHost() {
const deferred = new Deferred<string>();
const deferred = new Deferred<{ ip: string; manual?: boolean } | { ip?: string; manual: true }>();
const disposables: Array<Disposable> = [];

const discoveryTime = 5_000;
Expand All @@ -461,65 +448,64 @@ export class BrightScriptDebugConfigurationProvider implements DebugConfiguratio
}
}

//allow the user to manually type an IP address
//detect if the user types an IP address into the picker and presses enter.
quickPick.onDidAccept(() => {
deferred.resolve(quickPick.value);
deferred.resolve({
ip: quickPick.value
});
});

//create a text-based spinner factory for use in the "loading ..." label
const generateSpinnerText = util.createTextSpinner(3);

const itemCache = new Map<string, QuickPickHostItem>();

const refreshListDebounced = debounce(() => refreshList(true), 400);
let activeChangesSinceRefresh = 0;
let activeItem: QuickPickItem;

const refreshList = (updateSpinnerText = false) => {
console.log('refreshList', { updateSpinnerText: updateSpinnerText });
const { activeItems } = quickPick;
let spinnerText = '';
if (this.activeDeviceManager.timeSinceLastDiscoveredDevice < discoveryTime) {
spinnerText = ` (searching ${generateSpinnerText(updateSpinnerText)})`;
refreshListDebounced();
// remember the currently active item so we can maintain active selection when refreshing the list
quickPick.onDidChangeActive((items) => {
// reset our activeChanges tracker since users cannot cause items.length to be 0 (meaning a refresh has just happened)
if (items.length === 0) {
activeChangesSinceRefresh = 0;
return;
}
if (activeChangesSinceRefresh > 0) {
activeItem = items[0];
}
activeChangesSinceRefresh++;
});

const itemCache = new Map<string, QuickPickHostItem>();
quickPick.show();
const refreshList = () => {
const items = this.createHostQuickPickList(
this.activeDeviceManager.getActiveDevices(),
this.activeDeviceManager.lastUsedDevice,
spinnerText,
itemCache
);
quickPick.items = items;

// highlight the first non-separator item
if (activeItems.length === 0) {
for (const item of items) {
if (item.kind !== vscode.QuickPickItemKind.Separator && item.device?.id !== manualHostItemId) {
quickPick.activeItems = [item];
break;
}
}
} else {
//restore previously highlighted item
quickPick.activeItems = activeItems;
// update the busy spinner based on how long it's been since the last discovered device
quickPick.busy = this.activeDeviceManager.timeSinceLastDiscoveredDevice < discoveryTime;
setTimeout(() => {
quickPick.busy = this.activeDeviceManager.timeSinceLastDiscoveredDevice < discoveryTime;
}, discoveryTime - this.activeDeviceManager.timeSinceLastDiscoveredDevice + 20);

// clear the activeItem if we can't find it in the list
if (!quickPick.items.includes(activeItem)) {
activeItem = undefined;
}

// if the user manually selected an item, re-focus that item now that we refreshed the list
if (activeItem) {
quickPick.activeItems = [activeItem];
}
quickPick.show();
// quickPick.show();
};

//anytime the device picker adds/removes a device, update the list
disposables.push(
this.activeDeviceManager.on('device-found', () => {
console.log('device found');
refreshList();
}),
this.activeDeviceManager.on('device-expire', () => {
console.log('device expire');
refreshList();
})
);
this.activeDeviceManager.on('device-found', refreshList, disposables);
this.activeDeviceManager.on('device-expired', refreshList, disposables);

quickPick.onDidHide(() => {
dispose();
deferred.reject(new Error('No host was selected'));

});

quickPick.onDidChangeSelection(selection => {
Expand All @@ -529,11 +515,11 @@ export class BrightScriptDebugConfigurationProvider implements DebugConfiguratio
// Handle separator selection
} else {
if (selectedItem.label === manualLabel) {
deferred.resolve(manualLabel);
deferred.resolve({ manual: true });
} else {
const device = (selectedItem as any).device as RokuDeviceDetails;
this.activeDeviceManager.lastUsedDevice = device;
deferred.resolve(device.ip);
deferred.resolve(device);
}
quickPick.dispose();
}
Expand All @@ -543,17 +529,26 @@ export class BrightScriptDebugConfigurationProvider implements DebugConfiguratio
refreshList();
const result = await deferred.promise;
dispose();
return result;
if (result?.manual === true) {
return this.promptForHostManual();
} else {
return result?.ip;
}
}

/**
* Generate the label used when showing "host" entries in a quick picker
* @param device the device containing all the info
* @returns a properly formatted host string
*/
private createHostLabel(device: RokuDeviceDetails) {
return `${device.ip} | ${device.deviceInfo['user-device-name']} - ${device.deviceInfo['serial-number']} - ${device.deviceInfo['model-number']}`;
}

/**
* Generate the item list for the `this.promptForHost()` call
*/
private createHostQuickPickList(devices: RokuDeviceDetails[], lastUsedDevice: RokuDeviceDetails, spinnerText: string, cache = new Map<string, QuickPickHostItem>()) {
private createHostQuickPickList(devices: RokuDeviceDetails[], lastUsedDevice: RokuDeviceDetails, cache = new Map<string, QuickPickHostItem>()) {
//the collection of items we will eventually return
let items: QuickPickHostItem[] = [];

Expand Down Expand Up @@ -595,8 +590,8 @@ export class BrightScriptDebugConfigurationProvider implements DebugConfiguratio
}

//include a divider between devices and "manual" option (only if we have devices)
if (spinnerText || lastUsedDevice || devices.length) {
items.push({ label: spinnerText.trim() || ' ', kind: vscode.QuickPickItemKind.Separator });
if (lastUsedDevice || devices.length) {
items.push({ label: ' ', kind: vscode.QuickPickItemKind.Separator });
}

// allow user to manually type an IP address
Expand Down
2 changes: 1 addition & 1 deletion src/viewProviders/OnlineDevicesViewProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class OnlineDevicesViewProvider implements vscode.TreeDataProvider<vscode
}
});

this.activeDeviceManager.on('device-expire', device => {
this.activeDeviceManager.on('device-expired', device => {
// Remove the device from the list
const foundIndex = this.devices.findIndex(x => x.id === device.id);
this.devices.splice(foundIndex, 1);
Expand Down

0 comments on commit 3a96ae6

Please sign in to comment.