/platform/platform-api/src/com/intellij/ui/tabs/impl/singleRow/SingleRowLayout.java

https://bitbucket.org/nbargnesi/idea · Java · 491 lines · 389 code · 87 blank · 15 comment · 125 complexity · 75aaf6051ab2f9b1fac8563eb7f5eeed MD5 · raw file

  1. /*
  2. * Copyright 2000-2012 JetBrains s.r.o.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package com.intellij.ui.tabs.impl.singleRow;
  17. import com.intellij.openapi.util.text.StringUtil;
  18. import com.intellij.ui.tabs.JBTabsPosition;
  19. import com.intellij.ui.tabs.TabInfo;
  20. import com.intellij.ui.tabs.TabsUtil;
  21. import com.intellij.ui.tabs.impl.*;
  22. import org.jetbrains.annotations.Nullable;
  23. import javax.swing.*;
  24. import java.awt.*;
  25. import java.awt.event.MouseAdapter;
  26. import java.awt.event.MouseEvent;
  27. import java.util.Collections;
  28. import java.util.Comparator;
  29. import java.util.List;
  30. public class SingleRowLayout extends TabLayout {
  31. final JBTabsImpl myTabs;
  32. public SingleRowPassInfo myLastSingRowLayout;
  33. private final SingleRowLayoutStrategy myTop;
  34. private final SingleRowLayoutStrategy myLeft;
  35. private final SingleRowLayoutStrategy myBottom;
  36. private final SingleRowLayoutStrategy myRight;
  37. public final MoreTabsIcon myMoreIcon = new MoreTabsIcon() {
  38. @Nullable
  39. protected Rectangle getIconRec() {
  40. return myLastSingRowLayout != null ? myLastSingRowLayout.moreRect : null;
  41. }
  42. @Override
  43. protected int getIconY(Rectangle iconRec) {
  44. return super.getIconY(iconRec) +
  45. (myTabs.getTabsPosition() == JBTabsPosition.bottom
  46. ? TabsUtil.ACTIVE_TAB_UNDERLINE_HEIGHT
  47. : -(TabsUtil.ACTIVE_TAB_UNDERLINE_HEIGHT / 2));
  48. }
  49. };
  50. public JPopupMenu myMorePopup;
  51. public final GhostComponent myLeftGhost = new GhostComponent(RowDropPolicy.first, RowDropPolicy.first);
  52. public final GhostComponent myRightGhost = new GhostComponent(RowDropPolicy.last, RowDropPolicy.first);
  53. private enum RowDropPolicy {
  54. first, last
  55. }
  56. private RowDropPolicy myRowDropPolicy = RowDropPolicy.first;
  57. @Override
  58. public boolean isSideComponentOnTabs() {
  59. return getStrategy().isSideComponentOnTabs();
  60. }
  61. @Override
  62. public ShapeTransform createShapeTransform(Rectangle labelRec) {
  63. return getStrategy().createShapeTransform(labelRec);
  64. }
  65. @Override
  66. public boolean isDragOut(TabLabel tabLabel, int deltaX, int deltaY) {
  67. return getStrategy().isDragOut(tabLabel, deltaX, deltaY);
  68. }
  69. public SingleRowLayout(final JBTabsImpl tabs) {
  70. myTabs = tabs;
  71. myTop = new SingleRowLayoutStrategy.Top(this);
  72. myLeft = new SingleRowLayoutStrategy.Left(this);
  73. myBottom = new SingleRowLayoutStrategy.Bottom(this);
  74. myRight = new SingleRowLayoutStrategy.Right(this);
  75. }
  76. SingleRowLayoutStrategy getStrategy() {
  77. switch (myTabs.getPresentation().getTabsPosition()) {
  78. case top:
  79. return myTop;
  80. case left:
  81. return myLeft;
  82. case bottom:
  83. return myBottom;
  84. case right:
  85. return myRight;
  86. }
  87. return null;
  88. }
  89. protected boolean checkLayoutLabels(SingleRowPassInfo data) {
  90. boolean layoutLabels = true;
  91. if (!myTabs.myForcedRelayout &&
  92. myLastSingRowLayout != null &&
  93. myLastSingRowLayout.contentCount == myTabs.getTabCount() &&
  94. myLastSingRowLayout.layoutSize.equals(myTabs.getSize()) &&
  95. myLastSingRowLayout.scrollOffset == getScrollOffset()) {
  96. for (TabInfo each : data.myVisibleInfos) {
  97. final TabLabel eachLabel = myTabs.myInfo2Label.get(each);
  98. if (!eachLabel.isValid()) {
  99. break;
  100. }
  101. if (myTabs.getSelectedInfo() == each) {
  102. if (eachLabel.getBounds().width != 0) {
  103. layoutLabels = false;
  104. }
  105. }
  106. }
  107. }
  108. return layoutLabels;
  109. }
  110. int getScrollOffset() {
  111. return 0;
  112. }
  113. public void scroll(int units) {
  114. }
  115. public int getScrollUnitIncrement() {
  116. return 0;
  117. }
  118. public void scrollSelectionInView() {
  119. }
  120. public LayoutPassInfo layoutSingleRow(List<TabInfo> visibleInfos) {
  121. if (JBEditorTabs.isAlphabeticalMode()) {
  122. Collections.sort(visibleInfos, new Comparator<TabInfo>() {
  123. @Override
  124. public int compare(TabInfo o1, TabInfo o2) {
  125. return StringUtil.naturalCompare(o1.getText(), o2.getText());
  126. }
  127. });
  128. }
  129. SingleRowPassInfo data = new SingleRowPassInfo(this, visibleInfos);
  130. final boolean layoutLabels = checkLayoutLabels(data);
  131. if (!layoutLabels) {
  132. data = myLastSingRowLayout;
  133. }
  134. final TabInfo selected = myTabs.getSelectedInfo();
  135. prepareLayoutPassInfo(data, selected);
  136. myTabs.resetLayout(layoutLabels || myTabs.isHideTabs());
  137. if (layoutLabels && !myTabs.isHideTabs()) {
  138. data.position = getStrategy().getStartPosition(data) - getScrollOffset();
  139. recomputeToLayout(data);
  140. layoutLabelsAndGhosts(data);
  141. layoutMoreButton(data);
  142. }
  143. if (selected != null) {
  144. data.comp = selected.getComponent();
  145. getStrategy().layoutComp(data);
  146. }
  147. updateMoreIconVisibility(data);
  148. data.tabRectangle = new Rectangle();
  149. if (data.toLayout.size() > 0) {
  150. final TabLabel firstLabel = myTabs.myInfo2Label.get(data.toLayout.get(0));
  151. final TabLabel lastLabel = findLastVisibleLabel(data);
  152. if (firstLabel != null && lastLabel != null) {
  153. data.tabRectangle.x = firstLabel.getBounds().x;
  154. data.tabRectangle.y = firstLabel.getBounds().y;
  155. data.tabRectangle.width = (int)lastLabel.getBounds().getMaxX() - data.tabRectangle.x;
  156. data.tabRectangle.height = (int)lastLabel.getBounds().getMaxY() - data.tabRectangle.y;
  157. }
  158. }
  159. myLastSingRowLayout = data;
  160. return data;
  161. }
  162. @Nullable
  163. protected TabLabel findLastVisibleLabel(SingleRowPassInfo data) {
  164. return myTabs.myInfo2Label.get(data.toLayout.get(data.toLayout.size() - 1));
  165. }
  166. protected void prepareLayoutPassInfo(SingleRowPassInfo data, TabInfo selected) {
  167. data.insets = myTabs.getLayoutInsets();
  168. final JBTabsImpl.Toolbar selectedToolbar = myTabs.myInfo2Toolbar.get(selected);
  169. data.hToolbar = selectedToolbar != null && myTabs.myHorizontalSide && !selectedToolbar.isEmpty() ? selectedToolbar : null;
  170. data.vToolbar = selectedToolbar != null && !myTabs.myHorizontalSide && !selectedToolbar.isEmpty() ? selectedToolbar : null;
  171. data.toFitLength = getStrategy().getToFitLength(data);
  172. if (myTabs.isGhostsAlwaysVisible()) {
  173. data.toFitLength -= JBTabsImpl.getGhostTabLength() * 2 + (JBTabsImpl.getInterTabSpaceLength() * 2);
  174. }
  175. }
  176. protected void updateMoreIconVisibility(SingleRowPassInfo data) {
  177. myMoreIcon.setPainted(data.toLayout.size() > 0 && data.myVisibleInfos.size() > data.toLayout.size());
  178. }
  179. protected void layoutMoreButton(SingleRowPassInfo data) {
  180. if (data.toDrop.size() > 0) {
  181. data.moreRect = getStrategy().getMoreRect(data);
  182. }
  183. }
  184. private void layoutLabelsAndGhosts(final SingleRowPassInfo data) {
  185. if (data.firstGhostVisible || myTabs.isGhostsAlwaysVisible()) {
  186. data.firstGhost = getStrategy().getLayoutRect(data, data.position, JBTabsImpl.getGhostTabLength());
  187. myTabs.layout(myLeftGhost, data.firstGhost);
  188. data.position += getStrategy().getLengthIncrement(data.firstGhost.getSize()) + JBTabsImpl.getInterTabSpaceLength();
  189. }
  190. int deltaToFit = 0;
  191. if (data.firstGhostVisible || data.lastGhostVisible) {
  192. if (data.requiredLength < data.toFitLength && getStrategy().canBeStretched()) {
  193. deltaToFit = (int)Math.floor((data.toFitLength - data.requiredLength) / (double)data.toLayout.size());
  194. }
  195. }
  196. int totalLength = 0;
  197. int positionStart = data.position;
  198. boolean layoutStopped = false;
  199. for (TabInfo eachInfo : data.toLayout) {
  200. final TabLabel label = myTabs.myInfo2Label.get(eachInfo);
  201. if (layoutStopped) {
  202. label.setActionPanelVisible(false);
  203. final Rectangle rec = getStrategy().getLayoutRect(data, 0, 0);
  204. myTabs.layout(label, rec);
  205. continue;
  206. }
  207. label.setActionPanelVisible(true);
  208. final Dimension eachSize = label.getPreferredSize();
  209. boolean isLast = data.toLayout.indexOf(eachInfo) == data.toLayout.size() - 1;
  210. int length;
  211. if (!isLast || deltaToFit == 0) {
  212. length = getStrategy().getLengthIncrement(eachSize) + deltaToFit;
  213. }
  214. else {
  215. length = data.toFitLength - totalLength;
  216. }
  217. boolean continueLayout = applyTabLayout(data, label, length, deltaToFit);
  218. data.position = getStrategy().getMaxPosition(label.getBounds());
  219. data.position += JBTabsImpl.getInterTabSpaceLength();
  220. totalLength = getStrategy().getMaxPosition(label.getBounds()) - positionStart + JBTabsImpl.getInterTabSpaceLength();
  221. if (!continueLayout) {
  222. layoutStopped = true;
  223. }
  224. }
  225. for (TabInfo eachInfo : data.toDrop) {
  226. JBTabsImpl.resetLayout(myTabs.myInfo2Label.get(eachInfo));
  227. }
  228. if (data.lastGhostVisible || myTabs.isGhostsAlwaysVisible()) {
  229. data.lastGhost = getStrategy().getLayoutRect(data, data.position, JBTabsImpl.getGhostTabLength());
  230. myTabs.layout(myRightGhost, data.lastGhost);
  231. }
  232. }
  233. protected boolean applyTabLayout(SingleRowPassInfo data, TabLabel label, int length, int deltaToFit) {
  234. final Rectangle rec = getStrategy().getLayoutRect(data, data.position, length);
  235. myTabs.layout(label, rec);
  236. label.setAlignmentToCenter((deltaToFit > 0 || myTabs.isEditorTabs()) && getStrategy().isToCenterTextWhenStretched());
  237. return true;
  238. }
  239. protected void recomputeToLayout(final SingleRowPassInfo data) {
  240. calculateRequiredLength(data);
  241. while (true) {
  242. if (data.requiredLength <= data.toFitLength - data.position) break;
  243. if (data.toLayout.size() == 0) break;
  244. final TabInfo first = data.toLayout.get(0);
  245. final TabInfo last = data.toLayout.get(data.toLayout.size() - 1);
  246. if (myRowDropPolicy == RowDropPolicy.first) {
  247. if (first != myTabs.getSelectedInfo()) {
  248. processDrop(data, first, true);
  249. }
  250. else if (last != myTabs.getSelectedInfo()) {
  251. processDrop(data, last, false);
  252. }
  253. else {
  254. break;
  255. }
  256. }
  257. else {
  258. if (last != myTabs.getSelectedInfo()) {
  259. processDrop(data, last, false);
  260. }
  261. else if (first != myTabs.getSelectedInfo()) {
  262. processDrop(data, first, true);
  263. }
  264. else {
  265. break;
  266. }
  267. }
  268. }
  269. for (int i = 1; i < data.myVisibleInfos.size() - 1; i++) {
  270. final TabInfo each = data.myVisibleInfos.get(i);
  271. final TabInfo prev = data.myVisibleInfos.get(i - 1);
  272. final TabInfo next = data.myVisibleInfos.get(i + 1);
  273. if (data.toLayout.contains(each) && data.toDrop.contains(prev)) {
  274. myLeftGhost.setInfo(prev);
  275. }
  276. else if (data.toLayout.contains(each) && data.toDrop.contains(next)) {
  277. myRightGhost.setInfo(next);
  278. }
  279. }
  280. }
  281. protected void calculateRequiredLength(SingleRowPassInfo data) {
  282. for (TabInfo eachInfo : data.myVisibleInfos) {
  283. data.requiredLength += getRequiredLength(eachInfo);
  284. data.toLayout.add(eachInfo);
  285. }
  286. }
  287. protected int getRequiredLength(TabInfo eachInfo) {
  288. return getStrategy().getLengthIncrement(myTabs.myInfo2Label.get(eachInfo).getPreferredSize())
  289. + (myTabs.isEditorTabs() ? JBTabsImpl.getInterTabSpaceLength() : 0);
  290. }
  291. public boolean isTabHidden(TabInfo tabInfo) {
  292. return myLastSingRowLayout.toDrop.contains(tabInfo);
  293. }
  294. public class GhostComponent extends JLabel {
  295. private TabInfo myInfo;
  296. private GhostComponent(final RowDropPolicy before, final RowDropPolicy after) {
  297. addMouseListener(new MouseAdapter() {
  298. public void mousePressed(final MouseEvent e) {
  299. if (JBTabsImpl.isSelectionClick(e, true) && myInfo != null) {
  300. myRowDropPolicy = before;
  301. myTabs.select(myInfo, true).doWhenDone(new Runnable() {
  302. public void run() {
  303. myRowDropPolicy = after;
  304. }
  305. });
  306. } else {
  307. MouseEvent event = SwingUtilities.convertMouseEvent(e.getComponent(), e, myTabs);
  308. myTabs.processMouseEvent(event);
  309. }
  310. }
  311. });
  312. }
  313. public void setInfo(@Nullable final TabInfo info) {
  314. myInfo = info;
  315. setToolTipText(info != null ? info.getTooltipText() : null);
  316. }
  317. public void reset() {
  318. JBTabsImpl.resetLayout(this);
  319. setInfo(null);
  320. }
  321. }
  322. private void processDrop(final SingleRowPassInfo data, final TabInfo info, boolean isFirstSide) {
  323. data.requiredLength -= getStrategy().getLengthIncrement(myTabs.myInfo2Label.get(info).getPreferredSize());
  324. data.toDrop.add(info);
  325. data.toLayout.remove(info);
  326. if (data.toDrop.size() == 1) {
  327. data.toFitLength -= data.moreRectAxisSize;
  328. }
  329. if (!data.firstGhostVisible && isFirstSide) {
  330. data.firstGhostVisible = !myTabs.isEditorTabs();
  331. if (!myTabs.isGhostsAlwaysVisible() && !myTabs.isEditorTabs()) {
  332. data.toFitLength -= JBTabsImpl.getGhostTabLength();
  333. }
  334. }
  335. else if (!data.lastGhostVisible && !isFirstSide) {
  336. data.lastGhostVisible = !myTabs.isEditorTabs();
  337. if (!myTabs.isGhostsAlwaysVisible() && !myTabs.isEditorTabs()) {
  338. data.toFitLength -= JBTabsImpl.getGhostTabLength();
  339. }
  340. }
  341. }
  342. @Override
  343. public int getDropIndexFor(Point point) {
  344. if (myLastSingRowLayout == null) return -1;
  345. int result = -1;
  346. Component c = myTabs.getComponentAt(point);
  347. if (c instanceof JBTabsImpl) {
  348. for (int i = 0; i < myLastSingRowLayout.myVisibleInfos.size() - 1; i++) {
  349. TabLabel first = myTabs.myInfo2Label.get(myLastSingRowLayout.myVisibleInfos.get(i));
  350. TabLabel second = myTabs.myInfo2Label.get(myLastSingRowLayout.myVisibleInfos.get(i + 1));
  351. Rectangle firstBounds = first.getBounds();
  352. Rectangle secondBounds = second.getBounds();
  353. final boolean between;
  354. boolean horizontal = getStrategy() instanceof SingleRowLayoutStrategy.Horizontal;
  355. if (horizontal) {
  356. between = firstBounds.getMaxX() < point.x
  357. && secondBounds.getX() > point.x
  358. && firstBounds.y < point.y
  359. && secondBounds.getMaxY() > point.y;
  360. } else {
  361. between = firstBounds.getMaxY() < point.y
  362. && secondBounds.getY() > point.y
  363. && firstBounds.x < point.x
  364. && secondBounds.getMaxX() > point.x;
  365. }
  366. if (between) {
  367. c = first;
  368. break;
  369. }
  370. }
  371. }
  372. if (c instanceof TabLabel) {
  373. TabInfo info = ((TabLabel)c).getInfo();
  374. int index = myLastSingRowLayout.myVisibleInfos.indexOf(info);
  375. boolean isDropTarget = myTabs.isDropTarget(info);
  376. if (!isDropTarget) {
  377. for (int i = 0; i <= index; i++) {
  378. if (myTabs.isDropTarget(myLastSingRowLayout.myVisibleInfos.get(i))) {
  379. index -= 1;
  380. break;
  381. }
  382. }
  383. result = index;
  384. } else if (index < myLastSingRowLayout.myVisibleInfos.size()) {
  385. result = index;
  386. }
  387. } else if (c instanceof GhostComponent) {
  388. GhostComponent ghost = (GhostComponent)c;
  389. TabInfo info = ghost.myInfo;
  390. if (info != null) {
  391. int index = myLastSingRowLayout.myVisibleInfos.indexOf(info);
  392. index += myLeftGhost == ghost ? -1 : 1;
  393. result = index >= 0 && index < myLastSingRowLayout.myVisibleInfos.size() ? index : -1;
  394. } else {
  395. if (myLastSingRowLayout.myVisibleInfos.size() == 0) {
  396. result = 0;
  397. } else {
  398. result = myLeftGhost == ghost ? 0 : myLastSingRowLayout.myVisibleInfos.size() - 1;
  399. }
  400. }
  401. }
  402. return result;
  403. }
  404. }