Skip to content

Commit 125ddaf

Browse files
authored
Titlebar menu (#40)
- ADDED: new context menu when clicking on a tab's folder that allows to go back anywhere up to the root - IMPROVED: keep selected files/rename status even if the file being renamed has been renamed - IMPROVED: in Windows, show Ok button first (dialogs/alerts) - IMPROVED: made selected sideview outline more visible - FIXED: do not show keyboard outline when pressing cycling tabs with the kb
2 parents c8349d8 + 03cd064 commit 125ddaf

File tree

15 files changed

+283
-121
lines changed

15 files changed

+283
-121
lines changed

src/components/App.tsx

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@ import { PrefsDialog } from "./dialogs/PrefsDialog";
1818
import { HamburgerMenu } from "./HamburgerMenu";
1919
import { ShortcutsDialog } from "./dialogs/ShortcutsDialog";
2020
import { shouldCatchEvent, isEditable } from "../utils/dom";
21-
import { WithMenuAccelerators, Accelerators, Accelerator } from "./WithMenuAccelerators";
21+
import { WithMenuAccelerators, Accelerators, Accelerator, sendFakeCombo } from "./WithMenuAccelerators";
2222
import { remote } from 'electron';
23-
import { isPackage } from '../utils/platform';
23+
import { isPackage, isWin } from '../utils/platform';
2424
import { TabDescriptor } from "./TabList";
2525

2626
require("@blueprintjs/core/lib/css/blueprint.css");
2727
require("@blueprintjs/icons/lib/css/blueprint-icons.css");
2828
require("../css/main.css");
29+
require("../css/windows.css");
2930

3031
interface IState {
3132
isPrefsOpen: boolean;
@@ -37,9 +38,9 @@ interface InjectedProps extends WithNamespaces {
3738
settingsState: SettingsState
3839
}
3940

40-
const EXIT_DELAY = 1200;
41-
const KEY_Q = 81;
42-
const UP_DELAY = 130;
41+
enum KEYS {
42+
TAB = 9
43+
};
4344

4445
declare var ENV: any;
4546

@@ -95,7 +96,19 @@ class App extends React.Component<WithNamespaces, IState> {
9596
}
9697

9798
onShortcutsCombo = (e: KeyboardEvent) => {
98-
if (shouldCatchEvent(e) && e.which === 191 && e.shiftKey) {
99+
// Little hack to prevent pressing tab key from focus an element:
100+
// we prevent the propagation of the tab key keydown event
101+
// but this will then prevent the menu accelerators from working
102+
// so we simply send a fakeCombo to conter that.
103+
// We could simply disable outline using css but we want to keep
104+
// the app accessible.
105+
if (e.ctrlKey && e.keyCode === KEYS.TAB) {
106+
e.stopPropagation();
107+
e.stopImmediatePropagation();
108+
e.preventDefault();
109+
const combo = e.shiftKey ? 'Ctrl+Shift+Tab' : 'Ctrl+Tab';
110+
sendFakeCombo(combo);
111+
} else if (shouldCatchEvent(e) && e.which === 191 && e.shiftKey) {
99112
console.log('stopPropagation');
100113
e.stopPropagation();
101114
e.stopImmediatePropagation();
@@ -221,6 +234,7 @@ class App extends React.Component<WithNamespaces, IState> {
221234
// listen for events from main process
222235
this.addListeners();
223236
this.setDarkTheme();
237+
this.setPlatformClass();
224238
}
225239

226240
componentWillUnmount() {
@@ -424,7 +438,7 @@ class App extends React.Component<WithNamespaces, IState> {
424438
onDebugCache = () => {
425439
let i = 0;
426440
for (let cache of this.appState.views[0].caches) {
427-
console.log('cache', cache.selected.length, cache.selected, cache.position, cache.selectedId);
441+
console.log('cache', cache.selected.length, cache.selected, cache.selectedId, cache.editingId);
428442
}
429443
}
430444

@@ -576,6 +590,12 @@ class App extends React.Component<WithNamespaces, IState> {
576590
}
577591
}
578592

593+
setPlatformClass() {
594+
if (isWin) {
595+
document.body.classList.add('windows');
596+
}
597+
}
598+
579599
render() {
580600
const { isShortcutsOpen, isPrefsOpen, isExitDialogOpen } = this.state;
581601
const { settingsState } = this.injected;

src/components/ContextMenu.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ interface IState {
1111
}
1212

1313
export class ContextMenu extends React.Component<IProps, IState> {
14-
menu: Menu;
14+
menu: Menu = null;
1515

1616
constructor(props: IProps) {
1717
super(props);
@@ -21,10 +21,16 @@ export class ContextMenu extends React.Component<IProps, IState> {
2121
};
2222

2323
// generate menu
24-
this.menu = remote.Menu.buildFromTemplate(props.template);
24+
if (props.template) {
25+
this.menu = remote.Menu.buildFromTemplate(props.template);
26+
}
2527
}
2628

27-
showMenu() {
29+
showMenu(template: MenuItemConstructorOptions[] = null) {
30+
if (template) {
31+
this.menu = remote.Menu.buildFromTemplate(template);
32+
}
33+
2834
const window = remote.getCurrentWindow();
2935
this.menu.popup({
3036
window

src/components/FileTable.tsx

Lines changed: 57 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export class FileTableClass extends React.Component<IProps, IState> {
113113
nodes: [],// this.buildNodes(this.cache.files, false),
114114
selected: 0,
115115
type: 'local',
116-
position: cache.position,
116+
position: -1,
117117
path: cache.path
118118
};
119119

@@ -158,8 +158,6 @@ export class FileTableClass extends React.Component<IProps, IState> {
158158
if (scrollTop > 0) {
159159
this.tableRef.current.scrollToPosition(scrollTop);
160160
}
161-
162-
// this.cache.position = this.state.position;
163161
}
164162

165163
renderMenuAccelerators() {
@@ -216,6 +214,8 @@ export class FileTableClass extends React.Component<IProps, IState> {
216214
// when cache is being (re)loaded, cache.files is empty:
217215
// we don't want to show "empty folder" placeholder in that
218216
// that case, only when cache is loaded and there are no files
217+
const { viewState } = this.injected;
218+
console.log('reaction', viewState.viewId);
219219
if (cache.cmd === 'cwd' || cache.history.length) {
220220
this.updateNodes(files);
221221
}
@@ -236,7 +236,7 @@ export class FileTableClass extends React.Component<IProps, IState> {
236236
icon: file.isDir && "folder-close" || (filetype && TYPE_ICONS[filetype] || TYPE_ICONS['any']),
237237
name: file.fullname,
238238
nodeData: file,
239-
className: file.fullname !== '..' && file.fullname.startsWith('.') && 'isHidden' || '',
239+
className: file.fullname.startsWith('.') && 'isHidden' || '',
240240
isSelected: isSelected,
241241
size: !file.isDir && formatBytes(file.length) || '--'
242242
};
@@ -255,15 +255,6 @@ export class FileTableClass extends React.Component<IProps, IState> {
255255
.concat(files.sort(SortFn))
256256
.map((file, i) => this.buildNodeFromFile(file, keepSelection));
257257

258-
// append parent element
259-
const path = this.cache.path;
260-
261-
// TODO: when enabling ftp again, there may be something wrong
262-
// with the dir of the parent element added here
263-
if (!this.cache.isRoot(path)) {
264-
const node = this.buildNodeFromFile(this.cache.getParent(path), keepSelection);
265-
nodes.unshift(node);
266-
}
267258
console.timeEnd('buildingNodes');
268259

269260
return nodes;
@@ -291,11 +282,17 @@ export class FileTableClass extends React.Component<IProps, IState> {
291282
private updateState(nodes: ITableRow[], keepSelection = false) {
292283
const cache = this.cache;
293284
const newPath = nodes.length && nodes[0].nodeData.dir || '';
294-
// TODO: retrieve cursor selection: this may have changed if:
295-
// - cache have change (new files, files renamed, ...)
296-
// - sort method/order has changed
297285
const position = keepSelection && this.getFilePosition(nodes, cache.selectedId) || -1;
298-
this.setState({ nodes, selected: keepSelection ? this.state.selected : 0, position, path: newPath });
286+
console.log('setState 1', position);
287+
// cancel inlineedit if there was one
288+
this.clearEditElement();
289+
this.setState({ nodes, selected: keepSelection ? this.state.selected : 0, position, path: newPath }, () => {
290+
console.log('setState 1 done', keepSelection, cache.editingId, position);
291+
if (keepSelection && cache.editingId && position > -1) {
292+
console.log('*** need to restore edit id!');
293+
this.getElementAndToggleRename(undefined, false);
294+
}
295+
});
299296
}
300297

301298
getFilePosition(nodes: ITableRow[], id: FileID): number {
@@ -359,6 +356,15 @@ export class FileTableClass extends React.Component<IProps, IState> {
359356
}
360357
}
361358

359+
setEditElement(element: HTMLElement, file: File) {
360+
const cache = this.cache;
361+
362+
this.editingElement = element;
363+
this.editingFile = file;
364+
365+
cache.setEditingFile(file);
366+
}
367+
362368
setSort(newMethod: TSORT_METHOD_NAME, newOrder: TSORT_ORDER) {
363369
this.cache.setSort(newMethod, newOrder);
364370
}
@@ -387,7 +393,7 @@ export class FileTableClass extends React.Component<IProps, IState> {
387393
const element = event.target as HTMLElement;
388394

389395
// do not select parent dir pseudo file
390-
if (this.editingElement === element || file.fullname === '..') {
396+
if (this.editingElement === element) {
391397
return;
392398
}
393399

@@ -417,7 +423,7 @@ export class FileTableClass extends React.Component<IProps, IState> {
417423
// will be -1 if no left selected node is
418424
position = nodes.findIndex((node) => node.isSelected);
419425
}
420-
this.editingElement = null;
426+
this.setEditElement(null, null);
421427
}
422428

423429

@@ -427,6 +433,7 @@ export class FileTableClass extends React.Component<IProps, IState> {
427433
newSelected--;
428434
}
429435

436+
console.log('setState 2', position);
430437
this.setState({ nodes, selected: newSelected, position }, () => {
431438
this.updateSelection();
432439
});
@@ -461,8 +468,7 @@ export class FileTableClass extends React.Component<IProps, IState> {
461468
});
462469
});
463470
}
464-
this.editingElement = null;
465-
this.editingFile = null;
471+
this.setEditElement(null, null);
466472

467473
editingElement.blur();
468474
editingElement.removeAttribute('contenteditable');
@@ -496,29 +502,32 @@ export class FileTableClass extends React.Component<IProps, IState> {
496502
selection.addRange(range);
497503
}
498504

499-
toggleInlineRename(element: HTMLElement, originallySelected: boolean, file: File) {
505+
clearContentEditable() {
506+
if (this.editingElement) {
507+
this.editingElement.blur();
508+
this.editingElement.removeAttribute('contenteditable');
509+
}
510+
}
500511

512+
toggleInlineRename(element: HTMLElement, originallySelected: boolean, file: File, selectText = true) {
501513
console.log('toggle inlinerename');
502514
if (!file.readonly) {
503515
if (originallySelected) {
504516
console.log('activate inline rename!');
505517
element.contentEditable = "true";
506518
element.focus();
507-
this.editingElement = element;
508-
this.editingFile = file;
509-
this.selectLeftPart();
519+
this.setEditElement(element, file);
520+
selectText && this.selectLeftPart();
510521
element.onblur = () => {
522+
console.log('onblur!!');
511523
if (this.editingElement) {
512524
this.onInlineEdit(true);
513525
}
514526
}
515527
} else {
516528
// clear rename
517-
if (this.editingElement) {
518-
this.editingElement.blur();
519-
this.editingElement.removeAttribute('contenteditable');
520-
this.editingElement = null;
521-
}
529+
this.clearContentEditable();
530+
this.setEditElement(null, null);
522531
}
523532
}
524533
}
@@ -579,6 +588,7 @@ export class FileTableClass extends React.Component<IProps, IState> {
579588
i++;
580589
}
581590

591+
console.log('setState 3', position);
582592
this.setState({ nodes, selected, position }, () => {
583593
this.updateSelection();
584594
});
@@ -633,22 +643,32 @@ export class FileTableClass extends React.Component<IProps, IState> {
633643
return this.gridElement.querySelector(selector);
634644
}
635645

646+
clearEditElement() {
647+
const selector = `[aria-rowindex] [contenteditable]`;
648+
const element = this.gridElement.querySelector(selector) as HTMLElement;
649+
if (element) {
650+
element.onblur = null;
651+
element.removeAttribute('contenteditable');
652+
}
653+
}
654+
636655
isViewActive(): boolean {
637656
const { viewState } = this.injected;
638657
return viewState.isActive && !this.props.hide;
639658
}
640659

641-
getElementAndToggleRename = (e?: KeyboardEvent | string) => {
642-
if (!this.editingElement && this.state.selected > 0) {
660+
getElementAndToggleRename = (e?: KeyboardEvent | string, selectText = true) => {
661+
if (this.state.selected > 0) {
643662
const { position, nodes } = this.state;
644663
const node = nodes[position];
645664
const file = nodes[position].nodeData as File;
646665
const element = this.getNodeContentElement(position + 1);
666+
console.log('got element', position + 1, element, nodes.length);
647667
const span: HTMLElement = element.querySelector(`.${LABEL_CLASSNAME}`);
648668
if (e && typeof e !== 'string') {
649669
e.preventDefault();
650670
}
651-
this.toggleInlineRename(span, node.isSelected, file);
671+
this.toggleInlineRename(span, node.isSelected, file, selectText);
652672
}
653673
}
654674

@@ -681,27 +701,20 @@ export class FileTableClass extends React.Component<IProps, IState> {
681701
const file = node.nodeData as File;
682702

683703
if (!fileCache.isRoot(file.dir)) {
684-
this.cache.cd(file.dir, '..');
704+
this.cache.openParentDirectory();
685705
}
686706
}
687707
break;
688708
}
689709
}
690710

691711
moveSelection(step: number, isShiftDown: boolean) {
692-
const fileCache = this.cache;
693712
let { position, selected } = this.state;
694713
let { nodes } = this.state;
695714

715+
console.log('moveSelection', position);
696716
position += step;
697-
698-
// skip parent entry (only if this is not the root folder)
699-
if (!position) {
700-
const dir = (nodes[0].nodeData as File).dir;
701-
if (!fileCache.isRoot(dir)) {
702-
position += step;
703-
}
704-
}
717+
console.log('moveSelection apres', position);
705718

706719
if (position > -1 && position <= this.state.nodes.length - 1) {
707720
if (isShiftDown) {
@@ -714,6 +727,7 @@ export class FileTableClass extends React.Component<IProps, IState> {
714727

715728
nodes[position].isSelected = true;
716729

730+
console.log('setState 4', position);
717731
// move in method to reuse
718732
this.setState({ nodes, selected, position }, () => {
719733
this.updateSelection();

0 commit comments

Comments
 (0)