Skip to content

Commit 44d03a0

Browse files
committed
feat(ccb): reinstate active-file label with dirty indicator
Restores the vertical file-name + dirty-dot label that was removed in 22b2191 (replaced with a show-in-tree button at the time). The label hangs below the save group, reads the file name bottom-up, and shows a small amber dot at the top when the active document is dirty. Two improvements over the original: - Source the file from MainViewManager.getCurrentlyViewedFile(ACTIVE_PANE) instead of DocumentManager.getCurrentDocument(). getCurrentDocument() is CodeMirror-only, so the old label went blank whenever the active pane was an image or other non-CM view. Dirty state still goes through DocumentManager.getOpenDocumentForPath(fullPath), which is null for non-editable views — they correctly stay un-dirty. - Anti-pixelation for linux electron: swap `writing-mode: vertical-rl; transform: rotate(180deg)` for `writing-mode: sideways-lr`, which lets Chromium take its fast vertical-text path instead of rasterizing through a rotated transform. Pair it with translateZ(0), backface-visibility, -webkit-font-smoothing: antialiased, and text-rendering: geometricPrecision so the glyphs stay crisp even if a system falls back to the slow text path. Click still dispatches NAVIGATE_SHOW_IN_FILE_TREE (mirrors the binoculars affordance the label replaced). Tests: adds a 2b. #ccbFileLabel describe to CentralControlBar-integ-test covering render/placement, the name-shown / cleared-on-FILE_CLOSE_ALL cycle, the .is-dirty toggle via setText + refreshText, the click → NAVIGATE_SHOW_IN_FILE_TREE dispatch, and the label updating when the active file switches.
1 parent a7bb3ad commit 44d03a0

4 files changed

Lines changed: 211 additions & 1 deletion

File tree

src/index.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -969,6 +969,13 @@
969969
<i class="fa-solid fa-floppy-disk"></i>
970970
</a>
971971
</div>
972+
<div class="ccb-divider"></div>
973+
<div class="ccb-group ccb-group-file">
974+
<div id="ccbFileLabel" class="ccb-file-label">
975+
<span class="ccb-file-dot"></span>
976+
<span class="ccb-file-name"></span>
977+
</div>
978+
</div>
972979
</div>
973980

974981
<!--

src/styles/CentralControlBar.less

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@
4747
flex-direction: column;
4848
align-items: stretch;
4949
gap: 2px;
50+
51+
&.ccb-group-file {
52+
flex: 0 1 auto;
53+
max-height: 220px;
54+
overflow: hidden;
55+
justify-content: flex-start;
56+
padding: 4px 0;
57+
}
5058
}
5159

5260
.ccb-divider {
@@ -116,6 +124,61 @@
116124
}
117125
}
118126

127+
/* Vertical filename label — clicking reveals the current file in the
128+
file tree (keeps the old binoculars-on-label affordance). */
129+
.ccb-file-label {
130+
display: flex;
131+
flex-direction: column;
132+
align-items: center;
133+
gap: 4px;
134+
padding: 4px 0;
135+
color: @project-panel-text-2;
136+
height: 100%;
137+
overflow: hidden;
138+
cursor: pointer;
139+
140+
.ccb-file-dot {
141+
flex: 0 0 auto;
142+
line-height: 1;
143+
color: #f1b84e;
144+
visibility: hidden;
145+
font-size: 1rem;
146+
}
147+
148+
&.is-dirty .ccb-file-dot {
149+
visibility: visible;
150+
}
151+
152+
.ccb-file-name {
153+
flex: 1 1 auto;
154+
min-height: 0;
155+
/* `sideways-lr` rotates each glyph 90° CCW so the text reads
156+
bottom-up naturally. Using this instead of `vertical-rl` +
157+
`transform: rotate(180deg)` avoids the blurry sub-pixel
158+
rasterization that the transform path produced on linux
159+
electron, because Chromium can take its fast vertical-text
160+
path for glyph layout and skip the rotated bitmap upscale. */
161+
writing-mode: sideways-lr;
162+
white-space: nowrap;
163+
overflow: hidden;
164+
text-overflow: ellipsis;
165+
color: @project-panel-text-1;
166+
font-size: 13px;
167+
font-weight: 500;
168+
/* Promote to its own compositing layer + force AA — keeps the
169+
rotated glyphs crisp even when the system falls back to the
170+
slow text path. */
171+
transform: translateZ(0);
172+
backface-visibility: hidden;
173+
-webkit-font-smoothing: antialiased;
174+
text-rendering: geometricPrecision;
175+
}
176+
177+
&:hover .ccb-file-name {
178+
text-decoration: underline;
179+
}
180+
}
181+
119182
}
120183

121184
/* Editor collapse: actual layout handled in JS via .content width/visibility */

src/view/CentralControlBar.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ define(function (require, exports, module) {
2323
const AppInit = require("utils/AppInit");
2424
const CommandManager = require("command/CommandManager");
2525
const Commands = require("command/Commands");
26+
const DocumentManager = require("document/DocumentManager");
27+
const MainViewManager = require("view/MainViewManager");
2628
const Strings = require("strings");
2729
const WorkspaceManager = require("view/WorkspaceManager");
2830
const SidebarView = require("project/SidebarView");
@@ -32,6 +34,8 @@ define(function (require, exports, module) {
3234
let $bar;
3335
let $sidebar;
3436
let $content;
37+
let $fileLabel;
38+
let $fileName;
3539
let editorCollapsed = false;
3640
let savedToolbarWidth = null;
3741
let livePreviewWasOpen = false;
@@ -69,6 +73,35 @@ define(function (require, exports, module) {
6973
}
7074
}
7175

76+
function _updateFileLabel() {
77+
if (!$fileLabel) {
78+
return;
79+
}
80+
// Use MainViewManager instead of DocumentManager.getCurrentDocument()
81+
// so non-editable views (images, custom viewers, etc.) still show up
82+
// on the label. getCurrentDocument() is CodeMirror-backed only and
83+
// would leave the label blank whenever the active pane is an image
84+
// or other non-CM surface.
85+
const file = MainViewManager.getCurrentlyViewedFile(MainViewManager.ACTIVE_PANE);
86+
if (!file) {
87+
$fileLabel.removeClass("is-dirty");
88+
$fileName.text("");
89+
$fileLabel.attr("title", "");
90+
return;
91+
}
92+
const name = file.name || "";
93+
const fullPath = file.fullPath || "";
94+
const displayPath = fullPath && Phoenix && Phoenix.app && Phoenix.app.getDisplayPath
95+
? Phoenix.app.getDisplayPath(fullPath)
96+
: fullPath || name;
97+
$fileName.text(name);
98+
$fileLabel.attr("title", displayPath);
99+
// Dirty only makes sense for files with a document behind them; for
100+
// non-editable views there's no doc, so the class simply stays off.
101+
const doc = DocumentManager.getOpenDocumentForPath(fullPath);
102+
$fileLabel.toggleClass("is-dirty", !!(doc && doc.isDirty));
103+
}
104+
72105
function _executeCmd(id) {
73106
CommandManager.execute(id);
74107
}
@@ -263,6 +296,10 @@ define(function (require, exports, module) {
263296
e.preventDefault();
264297
_executeCmd(Commands.VIEW_HIDE_SIDEBAR);
265298
});
299+
$("#ccbFileLabel").on("click", function (e) {
300+
e.preventDefault();
301+
_executeCmd(Commands.NAVIGATE_SHOW_IN_FILE_TREE);
302+
});
266303
}
267304

268305
const _toggleDesignModeCommand = CommandManager.register(Strings.CMD_TOGGLE_DESIGN_MODE,
@@ -274,6 +311,8 @@ define(function (require, exports, module) {
274311
$bar = $("#centralControlBar");
275312
$sidebar = $("#sidebar");
276313
$content = $(".content");
314+
$fileLabel = $("#ccbFileLabel");
315+
$fileName = $fileLabel.find(".ccb-file-name");
277316

278317
_wireButtons();
279318
// The HTML titles on the control-bar buttons are fallback English
@@ -396,6 +435,11 @@ define(function (require, exports, module) {
396435
}
397436

398437
_updateSidebarToggleIcon();
438+
439+
MainViewManager.on("currentFileChange.ccb", _updateFileLabel);
440+
DocumentManager.on("dirtyFlagChange.ccb", _updateFileLabel);
441+
DocumentManager.on("pathDeleted.ccb fileNameChange.ccb", _updateFileLabel);
442+
_updateFileLabel();
399443
});
400444

401445

test/spec/CentralControlBar-integ-test.js

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
*
1919
*/
2020

21-
/*global describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, awaitsFor, awaits */
21+
/*global describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, awaitsFor, awaitsForDone, awaits */
2222

2323
define(function (require, exports, module) {
2424

@@ -292,6 +292,102 @@ define(function (require, exports, module) {
292292
});
293293
});
294294

295+
describe("2b. #ccbFileLabel (active-file indicator)", function () {
296+
let DocumentManager, MainViewManager;
297+
298+
beforeAll(function () {
299+
DocumentManager = brackets.test.DocumentManager;
300+
MainViewManager = brackets.test.MainViewManager;
301+
});
302+
303+
afterAll(async function () {
304+
// Leave the suite in the same "no file open" state we found it
305+
// in so later describes don't pick up a stray document.
306+
await awaitsForDone(
307+
CommandManager.execute(Commands.FILE_CLOSE_ALL, { _forceClose: true }),
308+
"close files opened by ccbFileLabel tests");
309+
});
310+
311+
it("should render #ccbFileLabel inside #centralControlBar", function () {
312+
const $label = _$("#ccbFileLabel");
313+
expect($label.length).toBe(1);
314+
expect($label.closest("#centralControlBar").length).toBe(1);
315+
expect($label.find(".ccb-file-name").length).toBe(1);
316+
expect($label.find(".ccb-file-dot").length).toBe(1);
317+
});
318+
319+
it("should show the active file's name and clear it when no file is open", async function () {
320+
await awaitsForDone(
321+
SpecRunnerUtils.openProjectFiles(["somelines.html"]),
322+
"open somelines.html");
323+
await awaitsFor(function () {
324+
return _$("#ccbFileLabel .ccb-file-name").text() === "somelines.html";
325+
}, "file label to show somelines.html", 2000);
326+
expect(_$("#ccbFileLabel").attr("title")).toContain("somelines.html");
327+
328+
await awaitsForDone(
329+
CommandManager.execute(Commands.FILE_CLOSE_ALL, { _forceClose: true }),
330+
"close all files");
331+
await awaitsFor(function () {
332+
return _$("#ccbFileLabel .ccb-file-name").text() === "";
333+
}, "file label to clear when no doc", 2000);
334+
});
335+
336+
it("should toggle .is-dirty as the active document's dirty flag changes", async function () {
337+
await awaitsForDone(
338+
SpecRunnerUtils.openProjectFiles(["somelines.html"]),
339+
"open somelines.html");
340+
await awaitsFor(function () {
341+
return _$("#ccbFileLabel .ccb-file-name").text() === "somelines.html";
342+
}, "file label primed", 2000);
343+
344+
expect(_$("#ccbFileLabel").hasClass("is-dirty")).toBe(false);
345+
346+
const doc = DocumentManager.getCurrentDocument();
347+
const originalText = doc.getText();
348+
doc.setText(originalText + "\n// ccb dirty probe");
349+
await awaitsFor(function () {
350+
return _$("#ccbFileLabel").hasClass("is-dirty");
351+
}, ".is-dirty to appear after setText", 2000);
352+
353+
doc.refreshText(originalText, doc.diskTimestamp);
354+
await awaitsFor(function () {
355+
return !_$("#ccbFileLabel").hasClass("is-dirty");
356+
}, ".is-dirty to clear after refresh", 2000);
357+
});
358+
359+
it("should dispatch NAVIGATE_SHOW_IN_FILE_TREE when clicked", async function () {
360+
await awaitsForDone(
361+
SpecRunnerUtils.openProjectFiles(["somelines.html"]),
362+
"open somelines.html");
363+
await awaitsFor(function () {
364+
return _$("#ccbFileLabel .ccb-file-name").text() === "somelines.html";
365+
}, "file label primed", 2000);
366+
367+
const executed = recordCommands(function () {
368+
_$("#ccbFileLabel").trigger("click");
369+
});
370+
expect(executed).toContain(Commands.NAVIGATE_SHOW_IN_FILE_TREE);
371+
});
372+
373+
it("should update when the active file switches", async function () {
374+
await awaitsForDone(
375+
SpecRunnerUtils.openProjectFiles(["somelines.html"]),
376+
"open somelines.html");
377+
await awaitsFor(function () {
378+
return _$("#ccbFileLabel .ccb-file-name").text() === "somelines.html";
379+
}, "somelines.html label", 2000);
380+
381+
await awaitsForDone(
382+
SpecRunnerUtils.openProjectFiles(["lotsOfLines.html"]),
383+
"open lotsOfLines.html");
384+
await awaitsFor(function () {
385+
return _$("#ccbFileLabel .ccb-file-name").text() === "lotsOfLines.html";
386+
}, "label to switch to lotsOfLines.html", 2000);
387+
expect(_$("#ccbFileLabel").attr("title")).toContain("lotsOfLines.html");
388+
});
389+
});
390+
295391
describe("3. Toggle Design Mode command", function () {
296392

297393
it("should execute VIEW_TOGGLE_DESIGN_MODE and flip isInDesignMode() from false to true and back", async function () {

0 commit comments

Comments
 (0)