| 1 | // Aseprite |
| 2 | // Copyright (C) 2019-2022 Igara Studio S.A. |
| 3 | // Copyright (C) 2001-2018 David Capello |
| 4 | // |
| 5 | // This program is distributed under the terms of |
| 6 | // the End-User License Agreement for Aseprite. |
| 7 | |
| 8 | #ifndef APP_UI_TABS_H_INCLUDED |
| 9 | #define APP_UI_TABS_H_INCLUDED |
| 10 | #pragma once |
| 11 | |
| 12 | #include "base/ref.h" |
| 13 | #include "ui/animated_widget.h" |
| 14 | #include "ui/widget.h" |
| 15 | |
| 16 | #include <memory> |
| 17 | #include <vector> |
| 18 | |
| 19 | namespace ui { |
| 20 | class Graphics; |
| 21 | class Overlay; |
| 22 | } |
| 23 | |
| 24 | namespace app { |
| 25 | class Tabs; |
| 26 | |
| 27 | enum class TabIcon { |
| 28 | NONE, |
| 29 | HOME, |
| 30 | }; |
| 31 | |
| 32 | // Required interface to be implemented by each new tab that is added |
| 33 | // in the Tabs widget. |
| 34 | class TabView { |
| 35 | public: |
| 36 | virtual ~TabView() { } |
| 37 | |
| 38 | // Returns the text to be shown in the tab. |
| 39 | virtual std::string getTabText() = 0; |
| 40 | |
| 41 | // Returns the icon to be shown in the tab |
| 42 | virtual TabIcon getTabIcon() = 0; |
| 43 | |
| 44 | // Returns the tab background color |
| 45 | virtual gfx::Color getTabColor() = 0; |
| 46 | }; |
| 47 | |
| 48 | enum class DropTabResult { |
| 49 | // The operation should be handled inside the Tabs widget (if the |
| 50 | // tab was dropped in the same Tabs, it will be moved or copied |
| 51 | // depending on what the user wants). |
| 52 | NOT_HANDLED, |
| 53 | |
| 54 | // The tab was docked in other place, so it must be removed from |
| 55 | // the specific Tabs widget. |
| 56 | REMOVE, |
| 57 | |
| 58 | // The operation was already handled, but the tab must not be |
| 59 | // removed from the Tabs (e.g. because it was cloned). |
| 60 | DONT_REMOVE, |
| 61 | }; |
| 62 | |
| 63 | enum class DropViewPreviewResult { |
| 64 | DROP_IN_PANEL, |
| 65 | DROP_IN_TABS, |
| 66 | FLOATING, |
| 67 | }; |
| 68 | |
| 69 | // Interface used to control notifications from the Tabs widget. |
| 70 | class TabsDelegate { |
| 71 | public: |
| 72 | |
| 73 | virtual ~TabsDelegate() { } |
| 74 | |
| 75 | // Returns true if the tab represent a modified document. |
| 76 | virtual bool isTabModified(Tabs* tabs, TabView* tabView) = 0; |
| 77 | |
| 78 | // Returns true if the tab can be cloned. |
| 79 | virtual bool canCloneTab(Tabs* tabs, TabView* tabView) = 0; |
| 80 | |
| 81 | // Called when the user selected the tab with the left mouse button. |
| 82 | virtual void onSelectTab(Tabs* tabs, TabView* tabView) = 0; |
| 83 | |
| 84 | // When the tab close button is pressed (or middle mouse button is used to close it). |
| 85 | virtual void onCloseTab(Tabs* tabs, TabView* tabView) = 0; |
| 86 | |
| 87 | // When the tab is cloned (this only happens when the user |
| 88 | // drag-and-drop the tab into the same Tabs with the Ctrl key). |
| 89 | virtual void onCloneTab(Tabs* tabs, TabView* tabView, int pos) = 0; |
| 90 | |
| 91 | // When the right-click is pressed in the tab. |
| 92 | virtual void (Tabs* tabs, TabView* tabView) = 0; |
| 93 | |
| 94 | // When the tab bar background is double-clicked. |
| 95 | virtual void onTabsContainerDoubleClicked(Tabs* tabs) = 0; |
| 96 | |
| 97 | // Called when the mouse is over a tab (the data can be null if the |
| 98 | // mouse just leave all tabs) |
| 99 | virtual void onMouseOverTab(Tabs* tabs, TabView* tabView) = 0; |
| 100 | |
| 101 | // Called when the mouse is leaving a tab |
| 102 | virtual void onMouseLeaveTab() = 0; |
| 103 | |
| 104 | // Called when the user is dragging a tab outside the Tabs |
| 105 | // bar. |
| 106 | virtual DropViewPreviewResult onFloatingTab(Tabs* tabs, |
| 107 | TabView* tabView, |
| 108 | const gfx::Point& screenPos) = 0; |
| 109 | |
| 110 | // Called when the user is dragging a tab inside the Tabs bar. |
| 111 | virtual void onDockingTab(Tabs* tabs, TabView* tabView) = 0; |
| 112 | |
| 113 | virtual DropTabResult onDropTab(Tabs* tabs, |
| 114 | TabView* tabView, |
| 115 | const gfx::Point& screenPos, |
| 116 | bool clone) = 0; |
| 117 | }; |
| 118 | |
| 119 | // Tabs control. Used to show opened documents. |
| 120 | class Tabs : public ui::Widget |
| 121 | , public ui::AnimatedWidget { |
| 122 | struct Tab { |
| 123 | TabView* view; |
| 124 | std::string text; |
| 125 | TabIcon icon; |
| 126 | gfx::Color color; |
| 127 | int x, width; |
| 128 | int oldX, oldWidth; |
| 129 | bool modified; |
| 130 | |
| 131 | Tab(TabView* view) : view(view) { |
| 132 | ASSERT(view); |
| 133 | text = view->getTabText(); |
| 134 | icon = view->getTabIcon(); |
| 135 | color = view->getTabColor(); |
| 136 | |
| 137 | x = width = oldX = oldWidth = |
| 138 | #if _DEBUG |
| 139 | 0xfefefefe; |
| 140 | #else |
| 141 | 0; |
| 142 | #endif |
| 143 | |
| 144 | modified = false; |
| 145 | } |
| 146 | }; |
| 147 | |
| 148 | typedef std::shared_ptr<Tab> TabPtr; |
| 149 | |
| 150 | typedef std::vector<TabPtr> TabsList; |
| 151 | typedef TabsList::iterator TabsListIterator; |
| 152 | |
| 153 | enum Ani : int { |
| 154 | ANI_NONE, |
| 155 | ANI_ADDING_TAB, |
| 156 | ANI_REMOVING_TAB, |
| 157 | ANI_REORDER_TABS |
| 158 | }; |
| 159 | |
| 160 | public: |
| 161 | static ui::WidgetType Type(); |
| 162 | |
| 163 | Tabs(TabsDelegate* delegate); |
| 164 | ~Tabs(); |
| 165 | |
| 166 | TabsDelegate* getDelegate() { return m_delegate; } |
| 167 | |
| 168 | void addTab(TabView* tabView, bool from_drop, int pos = -1); |
| 169 | void removeTab(TabView* tabView, bool with_animation); |
| 170 | void updateTabs(); |
| 171 | |
| 172 | // Returns true if the user can select other tab. |
| 173 | bool canSelectOtherTab() const; |
| 174 | |
| 175 | void selectTab(TabView* tabView); |
| 176 | void selectNextTab(); |
| 177 | void selectPreviousTab(); |
| 178 | TabView* getSelectedTab(); |
| 179 | |
| 180 | void setDockedStyle(); |
| 181 | |
| 182 | // Drop TabViews into this Tabs widget |
| 183 | void setDropViewPreview(const gfx::Point& screenPos, |
| 184 | TabView* view); |
| 185 | void removeDropViewPreview(); |
| 186 | int getDropTabIndex() const { return m_dropNewIndex; } |
| 187 | |
| 188 | protected: |
| 189 | bool onProcessMessage(ui::Message* msg) override; |
| 190 | void onInitTheme(ui::InitThemeEvent& ev) override; |
| 191 | void onPaint(ui::PaintEvent& ev) override; |
| 192 | void onResize(ui::ResizeEvent& ev) override; |
| 193 | void onSizeHint(ui::SizeHintEvent& ev) override; |
| 194 | void onAnimationFrame() override; |
| 195 | void onAnimationStop(int animation) override; |
| 196 | |
| 197 | private: |
| 198 | void resetOldPositions(); |
| 199 | void resetOldPositions(double t); |
| 200 | |
| 201 | void selectTabInternal(TabPtr& tab); |
| 202 | void drawTab(ui::Graphics* g, const gfx::Rect& box, Tab* tab, int dy, bool hover, bool selected); |
| 203 | void drawFiller(ui::Graphics* g, const gfx::Rect& box); |
| 204 | TabsListIterator getTabIteratorByView(TabView* tabView); |
| 205 | TabPtr getTabByView(TabView* tabView); |
| 206 | int getMaxScrollX(); |
| 207 | void makeTabVisible(Tab* tab); |
| 208 | void calculateHot(); |
| 209 | gfx::Rect getTabCloseButtonBounds(Tab* tab, const gfx::Rect& box); |
| 210 | void startDrag(); |
| 211 | void stopDrag(DropTabResult result); |
| 212 | gfx::Rect getTabBounds(Tab* tab); |
| 213 | void startReorderTabsAnimation(); |
| 214 | void startRemoveDragTabAnimation(); |
| 215 | void createFloatingOverlay(Tab* tab); |
| 216 | void destroyFloatingTab(); |
| 217 | void destroyFloatingOverlay(); |
| 218 | void updateMouseCursor(); |
| 219 | void updateDragTabIndexes(int mouseX, bool force_animation); |
| 220 | void updateDragCopyCursor(ui::Message* msg); |
| 221 | |
| 222 | // Specific variables about the style |
| 223 | int m_border; // Pixels used from the left side to draw the first tab |
| 224 | bool m_docked; // True if tabs are inside the workspace (not the main tabs panel) |
| 225 | int m_tabsHeight; // Number of pixels in Y-axis for each Tab |
| 226 | int m_tabsBottomHeight; // Number of pixels in the bottom part of Tabs widget |
| 227 | |
| 228 | // List of tabs (pointers to Tab instances). |
| 229 | TabsList m_list; |
| 230 | |
| 231 | // Which tab has the mouse over. |
| 232 | TabPtr m_hot; |
| 233 | |
| 234 | // True if the mouse is above the close button of m_hot tab. |
| 235 | bool m_hotCloseButton; |
| 236 | |
| 237 | // True if the user clicked over the close button of m_hot. |
| 238 | bool m_clickedCloseButton; |
| 239 | |
| 240 | // Current active tab. When this tab changes, the |
| 241 | // TabsDelegate::onSelectTab() is called. |
| 242 | TabPtr m_selected; |
| 243 | |
| 244 | // Delegate of notifications |
| 245 | TabsDelegate* m_delegate; |
| 246 | |
| 247 | // Variables for animation purposes |
| 248 | TabPtr m_addedTab; |
| 249 | TabPtr m_removedTab; |
| 250 | |
| 251 | //////////////////////////////////////// |
| 252 | // Drag-and-drop |
| 253 | |
| 254 | // True when the user is dragging a tab (the mouse must be |
| 255 | // captured when this flag is true). The dragging process doesn't |
| 256 | // start immediately, when the user clicks a tab the m_selected |
| 257 | // tab is changed, and then there is a threshold after we start |
| 258 | // dragging the tab. |
| 259 | bool m_isDragging; |
| 260 | |
| 261 | // True when the user is dragging the tab as a copy inside this |
| 262 | // same Tabs (e.g. using Ctrl key) This can be true only if |
| 263 | // TabsDelegate::canCloneTab() returns true for the tab being |
| 264 | // dragged. |
| 265 | bool m_dragCopy; |
| 266 | |
| 267 | // A copy of m_selected used only in the dragging process. This |
| 268 | // tab is not pointing to the same tab as m_selected. It's a copy |
| 269 | // because if m_dragCopy is true, we've to paint m_selected and |
| 270 | // m_dragTab separately (as two different tabs). |
| 271 | TabPtr m_dragTab; |
| 272 | |
| 273 | // Initial X position where m_dragTab/m_selected was when we |
| 274 | // started (mouse down) the drag process. |
| 275 | int m_dragTabX; |
| 276 | |
| 277 | // Initial mouse position when we start the dragging process. |
| 278 | gfx::Point m_dragMousePos; |
| 279 | |
| 280 | // New position where m_selected is being dragged. It's used to |
| 281 | // change the position of m_selected inside the m_list vector in |
| 282 | // real-time while the mouse is moved. |
| 283 | int m_dragTabIndex; |
| 284 | |
| 285 | // New position where a copyt of m_dragTab will be dropped. It |
| 286 | // only makes sense when m_dragCopy is true (the user is pressing |
| 287 | // Ctrl key and want to create a copy of the tab). |
| 288 | int m_dragCopyIndex; |
| 289 | |
| 290 | // Null if we are dragging the m_dragTab inside this Tabs widget, |
| 291 | // or non-null (equal to m_selected indeed), if we're moving the |
| 292 | // tab outside the Tabs widget (e.g. to dock the tabs in other |
| 293 | // location). |
| 294 | TabPtr m_floatingTab; |
| 295 | |
| 296 | // Overlay used to show the floating tab outside the Tabs widget |
| 297 | // (this overlay floats next to the mouse cursor). It's destroyed |
| 298 | // and recreated every time the tab is put inside or outside the |
| 299 | // Tabs widget. |
| 300 | base::Ref<ui::Overlay> m_floatingOverlay; |
| 301 | |
| 302 | // Relative mouse position inside the m_dragTab (used to adjust |
| 303 | // the m_floatingOverlay precisely). |
| 304 | gfx::Point m_floatingOffset; |
| 305 | |
| 306 | //////////////////////////////////////// |
| 307 | // Drop new tabs |
| 308 | |
| 309 | // Non-null when a foreign tab (a "TabView" from other "Tabs" |
| 310 | // widget), will be dropped in this specific Tabs widget. It's |
| 311 | // used only for feedback/UI purposes when the |
| 312 | // setDropViewPreview() is called. |
| 313 | TabView* m_dropNewTab; |
| 314 | int m_dropNewIndex; |
| 315 | int m_dropNewPosX; |
| 316 | }; |
| 317 | |
| 318 | } // namespace app |
| 319 | |
| 320 | #endif |
| 321 | |