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
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ x表示项目编号,y表示任务编号。
| 213 | 文档 |
| 214 | 插件协议 |
| 215 | 图形和图像处理 |
| 216 | 开始界面 |
1 change: 1 addition & 0 deletions TeXmacs/progs/init-research.scm
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
(lazy-menu (texmacs menus preferences-widgets) open-preferences)
(use-modules (texmacs menus main-menu))
(use-modules (texmacs menus tabpage-menu))
(use-modules (startup-tab startup-tab))
(lazy-define (texmacs menus file-menu) recent-file-list recent-directory-list)
(lazy-define (texmacs menus view-menu) set-bottom-bar test-bottom-bar?)
(tm-define (notify-set-attachment name key val) (noop))
Expand Down
21 changes: 21 additions & 0 deletions TeXmacs/progs/startup-tab/startup-tab.scm
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; MODULE : startup-tab.scm
;; DESCRIPTION : startup tab helpers
;; COPYRIGHT : (C) 2026 Yuki Lu
;;
;; This software falls under the GNU general public license version 3 or later.
;; It comes WITHOUT ANY WARRANTY WHATSOEVER. For details, see the file LICENSE
;; in the root directory or <http://www.gnu.org/licenses/gpl-3.0.html>.
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(texmacs-module (startup-tab startup-tab)
(:use (texmacs texmacs tm-files)))

(tm-define (startup-tab-enabled?)
#t)

(tm-define (startup-tab-default-entry)
"file")
96 changes: 96 additions & 0 deletions devel/216_1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# 216_1 启动标签页骨架 + 入口绑定

## 如何测试
- 编译时先输入 `xmake config -vD --startup_tab=true`
1. 启动 Mogan STEM,应该显示 "Mogan STEM" 标签页
2. 点击标签页应该显示 Backstage 界面(灰色背景,有 File/Template/Recent/Settings 按钮)
3. 新建文档应该创建新标签页
4. 切换标签页为 Mogan STEM 标签页时应该始终显示 Backstage,而不是编辑器
5. 关闭 Mogan STEM 标签页应该无效(受保护)

## 2026/03/17 启动标签页骨架实现与入口绑定

### What
实现 Mogan STEM 启动标签页(Backstage)功能,包括骨架实现、入口绑定、状态同步和代码优化。

#### 新增文件:

**src/Plugins/Qt/qt_startup_tab_widget.hpp** (新增)
- 定义 `QTStartupTabWidget` 类
- 定义 `Entry` 枚举:File, Template, Recent, Settings

**src/Plugins/Qt/qt_startup_tab_widget.cpp** (新增)
- 实现 `QTStartupTabWidget` 类
- 提供 Backstage 界面:灰色背景,File/Template/Recent/Settings 按钮
- 点击按钮切换当前 entry,更新标签文字
- 使用循环和数组简化按钮创建和连接

**TeXmacs/progs/startup-tab/startup-tab.scm** (新增)
- Scheme 层 startup-tab 支持

#### 修改文件:

**src/Plugins/Qt/qt_tm_widget.hpp** (修改)
- 添加 `startupContentWidget` 和 `startupTabMode` 成员变量
- 添加 `sync_startup_tab_mode()` 方法声明

**src/Plugins/Qt/qt_tm_widget.cpp** (修改)
- 添加 `is_startup_tab_file()` 和 `is_startup_tab_current_view()` 辅助函数
- 实现 `sync_startup_tab_mode()` 函数:
- 根据 `startupTabMode` 显示/隐藏 Backstage 或编辑器
- 提取 `show_widget_in_layout` 和 `hide_widget_from_layout` 辅助函数
- 在析构函数中删除 `startupContentWidget`,修复内存泄漏
- `SLOT_FILE` 处理:设置 `startupTabMode` 并调用 `sync_startup_tab_mode()`
- `SLOT_SCROLLABLE` 处理:只更新 `main_widget`,隐藏旧 widget,不调用 `sync_startup_tab_mode()`
- `update_visibility()`: 添加对 `startupTabMode` 的支持,隐藏工具栏

**src/Plugins/Qt/QTMTabPage.cpp** (修改)
- 启动页 tab 特殊化:固定到最左侧、限制最大宽度(`STARTUP_TAB_MAX_WIDTH`)
- 禁止启动页 tab 拖拽重排/跨窗口拖放
- 启动页 tab 不显示关闭按钮

**src/Texmacs/Data/new_window.hpp** (修改)
- `new_window()`: 添加 `force_tab_bar` 参数

**src/Texmacs/Data/new_window.cpp** (修改)
- `ensure_window()`: 添加 `USE_STARTUP_TAB` 支持
- 创建 `tmfs://startup-tab` buffer
- 将 `set_title_buffer` 移到 `window_set_view` 之前,确保标签页标题正确
- 调用 `new_window(true, geom, true)` 强制显示标签栏
- `kill_buffer()`: 防止删除 startup-tab

**src/Texmacs/Data/new_view.cpp** (修改)
- `attach_view()`: 将 `notify_set_view(u)` 移到 `vw->ed->resume()` 之前
- 确保 view 先加入 `view_history`,再刷新标签页菜单
- `window_set_view()`: 将 `set_window_url` 移到 `set_current_view` 之后
- 确保 `SLOT_FILE` 触发时 `get_current_view_safe()` 返回正确的 view
- `kill_tabpage()`: 防止删除 startup-tab

**src/System/Boot/init_texmacs.cpp** (修改)
- 启动阶段欢迎页触发条件调整为仅 `install_status == 1`
- 移除 `install_status == 2`相关代码
- 移除 `has_initial_welcome` 对 `ensure_window()` 的影响,确保无命令行文件时仍进入统一窗口创建流程

**src/System/config.h.xmake** (修改)
- 添加 `${define USE_STARTUP_TAB}` 宏

### Why
**目标**:为 Mogan 文档编辑器添加类似 Microsoft Word / WPS 的「文件页覆盖(Backstage)」与「模板中心」,在不新增独立启动壳层的前提下提升用户体验,降低新用户学习成本。

### How
**核心逻辑**:

1. **启动流程**:
- `ensure_window()` 检测无 buffer 时,创建 `tmfs://startup-tab` buffer
- 设置 buffer 标题为 "Mogan STEM"
- 创建窗口并设置 view

2. **界面切换**:
- `SLOT_SCROLLABLE`(`attach_view` 中):更新 `main_widget`,隐藏旧 widget
- `set_current_view`:更新 current view
- `SLOT_FILE`(`set_window_url` 中):设置 `startupTabMode`,调用 `sync_startup_tab_mode()`
- `sync_startup_tab_mode()`:根据 `startupTabMode` 显示/隐藏 Backstage 或编辑器

3. **状态判断**:
- `is_startup_tab_file(file)`:检查文件路径是否为 `tmfs://startup-tab`
- `is_startup_tab_current_view()`:检查 current view 的 buffer 是否为 startup-tab
103 changes: 80 additions & 23 deletions src/Plugins/Qt/QTMTabPage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
// The minimum width of a single tab page (in pixels).
const int MIN_TAB_PAGE_WIDTH= 150;
// The maximum width of a single tab page (in pixels).
const int MAX_TAB_PAGE_WIDTH= 200;
const int MAX_TAB_PAGE_WIDTH = 200;
const int STARTUP_TAB_MAX_WIDTH= 100;

// The horizontal padding for tab container (in pixels).
#ifdef Q_OS_MAC
Expand Down Expand Up @@ -100,6 +101,25 @@ url g_mostRecentlyDraggedTab= url_none ();
QTMTabPageContainer* g_mostRecentlyDraggedBar= nullptr;
QTMTabPageContainer* g_mostRecentlyEnteredBar= nullptr;

static url
startup_tab_buffer_name () {
return url ("tmfs://startup-tab");
}

static bool
is_startup_tab_view (url viewUrl) {
if (is_none (viewUrl)) return false;
return view_to_buffer (viewUrl) == startup_tab_buffer_name ();
}

static int
startup_tab_index (const QList<QTMTabPage*>& tabs) {
for (int i= 0; i < tabs.size (); ++i)
if (tabs[i] != nullptr && is_startup_tab_view (tabs[i]->m_viewUrl))
return i;
return -1;
}

/******************************************************************************
* QTMTabPage
******************************************************************************/
Expand Down Expand Up @@ -196,6 +216,9 @@ QTMTabPage::resizeEvent (QResizeEvent* e) {

void
QTMTabPage::mousePressEvent (QMouseEvent* e) {
if (is_startup_tab_view (m_viewUrl)) {
return QToolButton::mousePressEvent (e);
}
if (e->button () == Qt::LeftButton) {
g_mostRecentlyDraggedTab= this->m_viewUrl;
g_mostRecentlyDraggedBar=
Expand All @@ -208,6 +231,9 @@ QTMTabPage::mousePressEvent (QMouseEvent* e) {

void
QTMTabPage::mouseMoveEvent (QMouseEvent* e) {
if (is_startup_tab_view (m_viewUrl)) {
return QToolButton::mouseMoveEvent (e);
}
if (!(e->buttons () & Qt::LeftButton)) return QToolButton::mouseMoveEvent (e);
if ((e->pos () - m_dragStartPos).manhattanLength () < 3) {
// avoid treating small movement(more like a click) as dragging
Expand Down Expand Up @@ -255,8 +281,7 @@ QTMTabPage::leaveEvent (QEvent* e) {

void
QTMTabPage::updateCloseButtonVisibility () {
// 始终显示关闭按钮(无论是否选中或悬停)
bool shouldShow= true;
bool shouldShow= !is_startup_tab_view (m_viewUrl);
bool wasVisible= m_closeBtn->isVisible ();
m_closeBtn->setVisible (shouldShow);

Expand Down Expand Up @@ -356,6 +381,12 @@ QTMTabPageContainer::extractTabPages (QList<QAction*>* p_src) {
// which will be deleted by the parent widget (QTMTabPageBar) when it
// is destroyed (by shedule_destruction).
}

int startupIndex= startup_tab_index (m_tabPageList);
if (startupIndex > 0) {
QTMTabPage* startupTab= m_tabPageList.takeAt (startupIndex);
m_tabPageList.prepend (startupTab);
}
}

void
Expand Down Expand Up @@ -404,22 +435,26 @@ QTMTabPageContainer::arrangeTabPages () {

// Set new positions for all tabs
for (int i= 0; i < m_tabPageList.size (); ++i) {
QTMTabPage* tab= m_tabPageList[i];
QTMTabPage* tab = m_tabPageList[i];
int currentTabWidth= tabWidth;
if (is_startup_tab_view (tab->m_viewUrl)) {
currentTabWidth= std::min (tabWidth, STARTUP_TAB_MAX_WIDTH);
}

if (g_pointingIndex == i) {
// construct a dummy rectangle widget for indication of the inser place of
// the dragged tab
dummyTabPage->setGeometry (accumWidth, 0, tabWidth, m_rowHeight);
dummyTabPage->setGeometry (accumWidth, 0, currentTabWidth, m_rowHeight);
dummyTabPage->show ();
accumWidth+= tabWidth;
accumWidth+= currentTabWidth;
}
if (g_mostRecentlyClosedTab == tab->m_viewUrl) {
tab->hide ();
continue;
}

tab->setGeometry (accumWidth, 0, tabWidth, m_rowHeight);
accumWidth+= tabWidth;
tab->setGeometry (accumWidth, 0, currentTabWidth, m_rowHeight);
accumWidth+= currentTabWidth;
tab->show ();
}
if (g_pointingIndex >= m_tabPageList.size ()) {
Expand Down Expand Up @@ -473,26 +508,29 @@ QTMTabPageContainer::setHitTestVisibleForTabPages (
int
QTMTabPageContainer::mapToPointing (QDropEvent* e, QPoint& p_indicatorPos) {
QPoint pos= e->position ().toPoint ();
// Now we use g_tabWidth to calculate the pointing index, instead of geometry
if (g_tabWidth <= 0) {
if (m_tabPageList.isEmpty ()) {
p_indicatorPos= QPoint (0, 0);
return 0;
}
int index= pos.x () / g_tabWidth;
index = qMax (0, qMin (index, m_tabPageList.size ()));
if (index < m_tabPageList.size ()) {
QRect rect = m_tabPageList[index]->geometry ();

int index= m_tabPageList.size ();
for (int i= 0; i < m_tabPageList.size (); ++i) {
QTMTabPage* tab= m_tabPageList[i];
if (!tab || !tab->isVisible ()) continue;
QRect rect = tab->geometry ();
int x_mid= rect.x () + rect.width () / 2;
if (pos.x () >= x_mid) {
p_indicatorPos= rect.topRight ();
return std::min (index + 1, int (m_tabPageList.size ()));
if (pos.x () < x_mid) {
index = i;
p_indicatorPos= rect.topLeft ();
break;
}
p_indicatorPos= rect.topLeft ();
return index;
index = i + 1;
p_indicatorPos= rect.topRight ();
}
// no valid pointing tab, p_indicatorPos should be at the end
p_indicatorPos= m_tabPageList.last ()->geometry ().topRight ();
return m_tabPageList.size ();

int startupIndex= startup_tab_index (m_tabPageList);
if (startupIndex == 0) index= qMax (1, index);
return std::min (index, static_cast<int> (m_tabPageList.size ()));
}

void
Expand Down Expand Up @@ -533,6 +571,17 @@ QTMTabPageContainer::dropEvent (QDropEvent* e) {
QTMTabPage* draggingTab = m_tabPageList[m_draggingTabIndex];
int oldIndex = m_draggingTabIndex;
int newIndex= pointingIndex > oldIndex ? pointingIndex - 1 : pointingIndex;
int startupIndex= startup_tab_index (m_tabPageList);
if (startupIndex == oldIndex) {
g_mostRecentlyClosedTab= url_none ();
g_pointingIndex = -1;
m_draggingTabIndex = -1;
arrangeTabPages ();
m_indicator->hide ();
dummyTabPage->hide ();
return;
}
if (startupIndex == 0) newIndex= qMax (1, newIndex);
g_mostRecentlyClosedTab= url_none ();
g_pointingIndex = -1;

Expand All @@ -552,7 +601,15 @@ QTMTabPageContainer::dropEvent (QDropEvent* e) {
// Attach当前标签页到其他窗口
QObject* src= e->source ();
if (src && src != this) {
url dragged_view = g_mostRecentlyDraggedTab;
url dragged_view= g_mostRecentlyDraggedTab;
if (is_startup_tab_view (dragged_view)) {
g_mostRecentlyDraggedTab= url_none ();
g_mostRecentlyDraggedBar= nullptr;
g_pointingIndex = -1;
m_indicator->hide ();
dummyTabPage->hide ();
return;
}
tm_window dragged_window= concrete_view (dragged_view)->win_tabpage;
url target_view= m_tabPageList[0]->m_viewUrl; // 通过view来获取window
tm_window target_window= concrete_view (target_view)->win_tabpage;
Expand Down
Loading
Loading