ソースを参照

- Plot Area Tab view can now be toggled, added entry in View Menu and shortcut key CTRL+F10
- All the tabs in the GUI right side are (Plot Are, Preferences etc) are now detachable to a separate windows which when closed it returns in the previous location in the toolbar. Those detached tabs can be also reattached by drag and drop.

Marius Stanciu 7 年 前
コミット
f4da8c8c68
6 ファイル変更511 行追加64 行削除
  1. 35 3
      FlatCAMApp.py
  2. 23 60
      FlatCAMGUI.py
  3. 445 1
      GUIElements.py
  4. 6 0
      ObjectCollection.py
  5. 2 0
      README.md
  6. BIN
      share/plot32.png

+ 35 - 3
FlatCAMApp.py

@@ -1032,6 +1032,7 @@ class App(QtCore.QObject):
         self.ui.menuview_zoom_in.triggered.connect(lambda: self.plotcanvas.zoom(1 / 1.5))
         self.ui.menuview_zoom_out.triggered.connect(lambda: self.plotcanvas.zoom(1.5))
         self.ui.menuview_toggle_fscreen.triggered.connect(self.on_fullscreen)
+        self.ui.menuview_toggle_parea.triggered.connect(self.on_toggle_plotarea)
         self.ui.menuview_toggle_grid.triggered.connect(self.on_toggle_grid)
         self.ui.menuview_toggle_axis.triggered.connect(self.on_toggle_axis)
         self.ui.menuview_toggle_workspace.triggered.connect(self.on_workspace_menu)
@@ -2752,13 +2753,29 @@ class App(QtCore.QObject):
         if self.toggle_fscreen is False:
             for tb in self.ui.findChildren(QtWidgets.QToolBar):
                 tb.setVisible(False)
-            self.ui.notebook.setVisible(False)
+            self.ui.splitter_left.setVisible(False)
             self.toggle_fscreen = True
         else:
             self.restore_toolbar_view()
-            self.ui.notebook.setVisible(True)
+            self.ui.splitter_left.setVisible(True)
             self.toggle_fscreen = False
 
+    def on_toggle_plotarea(self):
+        try:
+            name = self.ui.plot_tab_area.widget(0).objectName()
+        except AttributeError:
+            self.ui.plot_tab_area.addTab(self.ui.plot_tab, "Plot Area")
+            # remove the close button from the Plot Area tab (first tab index = 0) as this one will always be ON
+            self.ui.plot_tab_area.protectTab(0)
+            return
+
+        if name != 'plotarea':
+            self.ui.plot_tab_area.insertTab(0, self.ui.plot_tab, "Plot Area")
+            # remove the close button from the Plot Area tab (first tab index = 0) as this one will always be ON
+            self.ui.plot_tab_area.protectTab(0)
+        else:
+            self.ui.plot_tab_area.closeTab(0)
+
     def on_toggle_axis(self):
         if self.toggle_axis is False:
             self.plotcanvas.v_line.set_data(color=(0.70, 0.3, 0.3, 1.0))
@@ -3751,6 +3768,10 @@ class App(QtCore.QObject):
             if event.key == 'S':
                 self.on_file_saveproject()
 
+            # Toggle Plot Area
+            if event.key == 'F10':
+                self.on_toggle_plotarea()
+
             return
         elif self.key_modifiers == QtCore.Qt.AltModifier:
             # place holder for further shortcut key
@@ -3791,6 +3812,7 @@ class App(QtCore.QObject):
             if event.key == 'F10':
                 self.on_fullscreen()
 
+            return
         elif self.key_modifiers == QtCore.Qt.ShiftModifier:
             # place holder for further shortcut key
 
@@ -3825,7 +3847,6 @@ class App(QtCore.QObject):
             if event.key == 'Y':
                 self.on_skewy()
 
-            return
         else:
             if event.key == 'F1':
                 webbrowser.open(self.manual_url)
@@ -3918,6 +3939,17 @@ class App(QtCore.QObject):
 
     def on_shortcut_list(self):
 
+        # add the tab if it was closed
+        self.ui.plot_tab_area.addTab(self.ui.shortcuts_tab, "Key Shortcut List")
+
+        # delete the absolute and relative position and messages in the infobar
+        self.ui.position_label.setText("")
+        self.ui.rel_position_label.setText("")
+
+        # Switch plot_area to preferences page
+        self.ui.plot_tab_area.setCurrentWidget(self.ui.shortcuts_tab)
+        self.ui.show()
+
         msg = '''<b>Shortcut list</b><br>
 <br>
 <b>~:</b>       Show Shortcut List<br>

+ 23 - 60
FlatCAMGUI.py

@@ -268,6 +268,8 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.menuview.addSeparator()
         self.menuview_toggle_fscreen = self.menuview.addAction(
             QtGui.QIcon('share/fscreen32.png'), "&Toggle FullScreen\tALT+F10")
+        self.menuview_toggle_parea = self.menuview.addAction(
+            QtGui.QIcon('share/plot32.png'), "&Toggle Plot Area\tCTRL+F10")
 
         self.menuview.addSeparator()
         self.menuview_toggle_grid = self.menuview.addAction(QtGui.QIcon('share/grid32.png'), "&Toggle Grid\tG")
@@ -408,52 +410,8 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.addToolBar(self.geo_edit_toolbar)
         self.snap_toolbar = QtWidgets.QToolBar('Grid Toolbar')
         self.snap_toolbar.setObjectName('Snap_TB')
+        # self.snap_toolbar.setMaximumHeight(30)
         self.addToolBar(self.snap_toolbar)
-        # if self.app.gui_defaults['global_theme'] == 'standard':
-        #     self.toolbarfile = QtWidgets.QToolBar('File Toolbar')
-        #     self.toolbarfile.setObjectName('File_TB')
-        #     self.addToolBar(self.toolbarfile)
-        #     self.toolbargeo = QtWidgets.QToolBar('Edit Toolbar')
-        #     self.toolbargeo.setObjectName('Edit_TB')
-        #     self.addToolBar(self.toolbargeo)
-        #     self.toolbarview = QtWidgets.QToolBar('View Toolbar')
-        #     self.toolbarview.setObjectName('View_TB')
-        #     self.addToolBar(self.toolbarview)
-        #     self.toolbartools = QtWidgets.QToolBar('Tools Toolbar')
-        #     self.toolbartools.setObjectName('Tools_TB')
-        #     self.addToolBar(self.toolbartools)
-        #     self.exc_edit_toolbar = QtWidgets.QToolBar('Excellon Editor Toolbar')
-        #     self.exc_edit_toolbar.setObjectName('ExcEditor_TB')
-        #     self.addToolBar(self.exc_edit_toolbar)
-        #     self.geo_edit_toolbar = QtWidgets.QToolBar('Geometry Editor Toolbar')
-        #     self.geo_edit_toolbar.setVisible(False)
-        #     self.geo_edit_toolbar.setObjectName('GeoEditor_TB')
-        #     self.addToolBar(self.geo_edit_toolbar)
-        #     self.snap_toolbar = QtWidgets.QToolBar('Grid Toolbar')
-        #     self.snap_toolbar.setObjectName('Snap_TB')
-        # elif self.app.defaults['global_theme'] == 'compact':
-        #     self.toolbarfile = QtWidgets.QToolBar('File Toolbar')
-        #     self.toolbarfile.setObjectName('File_TB')
-        #     self.addToolBar(self.toolbarfile)
-        #     self.toolbargeo = QtWidgets.QToolBar('Edit Toolbar')
-        #     self.toolbargeo.setObjectName('Edit_TB')
-        #     self.addToolBar(self.toolbargeo)
-        #     self.toolbarview = QtWidgets.QToolBar('View Toolbar')
-        #     self.toolbarview.setObjectName('View_TB')
-        #     self.addToolBar(self.toolbarview)
-        #     self.toolbartools = QtWidgets.QToolBar('Tools Toolbar')
-        #     self.toolbartools.setObjectName('Tools_TB')
-        #     self.addToolBar(self.toolbartools)
-        #     self.exc_edit_toolbar = QtWidgets.QToolBar('Excellon Editor Toolbar')
-        #     self.exc_edit_toolbar.setObjectName('ExcEditor_TB')
-        #     self.addToolBar(self.exc_edit_toolbar)
-        #     self.geo_edit_toolbar = QtWidgets.QToolBar('Geometry Editor Toolbar')
-        #     self.geo_edit_toolbar.setVisible(False)
-        #     self.geo_edit_toolbar.setObjectName('GeoEditor_TB')
-        #     self.addToolBar(self.geo_edit_toolbar)
-        #     self.snap_toolbar = QtWidgets.QToolBar('Grid Toolbar')
-        #     self.snap_toolbar.setObjectName('Snap_TB')
-        #     self.addToolBar(self.snap_toolbar)
 
         ### File Toolbar ###
         self.file_open_gerber_btn = self.toolbarfile.addAction(QtGui.QIcon('share/flatcam_icon32.png'),
@@ -580,15 +538,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.notebook = QtWidgets.QTabWidget()
         self.splitter.addWidget(self.notebook)
 
-        # if self.app.defaults['global_theme'] == 'standard':
-        #     self.splitter.addWidget(self.notebook)
-        # elif self.app.defaults['global_theme'] == 'compact':
-        #     self.splitter_2 = QtWidgets.QSplitter(Qt.Vertical)
-        #     self.splitter.addWidget(self.splitter_2)
-        #     self.splitter_2.addWidget(self.notebook)
-        #     self.splitter_2.setHandleWidth(0)
-        #     self.snap_toolbar.setMaximumHeight(30)
-        #     self.splitter_2.addWidget(self.snap_toolbar)
+        self.splitter_left = QtWidgets.QSplitter(Qt.Vertical)
+        self.splitter.addWidget(self.splitter_left)
+        self.splitter_left.addWidget(self.notebook)
+        self.splitter_left.setHandleWidth(0)
 
         ### Project ###
         self.project_tab = QtWidgets.QWidget()
@@ -620,16 +573,19 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.right_lay = QtWidgets.QVBoxLayout()
         self.right_lay.setContentsMargins(0, 0, 0, 0)
         self.right_widget.setLayout(self.right_lay)
-        self.plot_tab_area = FCTab()
+        # self.plot_tab_area = FCTab()
+        self.plot_tab_area = FCDetachableTab()
+
         self.right_lay.addWidget(self.plot_tab_area)
         self.plot_tab_area.setTabsClosable(True)
 
-        plot_tab = QtWidgets.QWidget()
-        self.plot_tab_area.addTab(plot_tab, "Plot Area")
+        self.plot_tab = QtWidgets.QWidget()
+        self.plot_tab.setObjectName("plotarea")
+        self.plot_tab_area.addTab(self.plot_tab, "Plot Area")
 
         self.right_layout = QtWidgets.QVBoxLayout()
         self.right_layout.setContentsMargins(2, 2, 2, 2)
-        plot_tab.setLayout(self.right_layout)
+        self.plot_tab.setLayout(self.right_layout)
 
         # remove the close button from the Plot Area tab (first tab index = 0) as this one will always be ON
         self.plot_tab_area.protectTab(0)
@@ -759,6 +715,13 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
             "which is the file storing the working default preferences.")
         self.pref_tab_bottom_layout_2.addWidget(self.pref_save_button)
 
+        ########################################
+        ### HERE WE BUILD THE SHORTCUTS LIST. TAB AREA ###
+        ########################################
+        self.shortcuts_tab = QtWidgets.QWidget()
+        self.sh_tab_layout = QtWidgets.QVBoxLayout(self.shortcuts_tab)
+        self.sh_tab_layout.setContentsMargins(2, 2, 2, 2)
+
 
         ##############################################################
         ### HERE WE BUILD THE CONTEXT MENU FOR RMB CLICK ON CANVAS ###
@@ -918,7 +881,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
 
         # restore the Toolbar State from file
         try:
-            with open(self.app.data_path + '\state.config', 'rb') as stream:
+            with open(self.app.data_path + '\gui_state.config', 'rb') as stream:
                 self.restoreState(QtCore.QByteArray(stream.read()))
             log.debug("FlatCAMGUI.__init__() --> UI state restored.")
         except IOError:
@@ -992,7 +955,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
 
         if self.app.should_we_quit is True:
             # save toolbar state to file
-            with open(self.app.data_path + '\state.config', 'wb') as stream:
+            with open(self.app.data_path + '\gui_state.config', 'wb') as stream:
                 stream.write(self.saveState().data())
                 log.debug("FlatCAMGUI.__init__() --> UI state saved.")
             QtWidgets.qApp.quit()

+ 445 - 1
GUIElements.py

@@ -1,4 +1,6 @@
-from PyQt5 import QtGui, QtCore, QtWidgets, QtWidgets
+from PyQt5 import QtGui, QtCore, QtWidgets
+from PyQt5.QtCore import pyqtSignal, pyqtSlot
+
 from copy import copy
 import re
 import logging
@@ -550,6 +552,448 @@ class FCTab(QtWidgets.QTabWidget):
         self.tabBar().setTabButton(currentIndex, QtWidgets.QTabBar.RightSide, None)
 
 
+class FCDetachableTab(QtWidgets.QTabWidget):
+    # From here: https://stackoverflow.com/questions/47267195/in-pyqt4-is-it-possible-to-detach-tabs-from-a-qtabwidget
+    def __init__(self, parent=None):
+
+        super().__init__()
+
+        self.tabBar = self.FCTabBar(self)
+        self.tabBar.onDetachTabSignal.connect(self.detachTab)
+        self.tabBar.onMoveTabSignal.connect(self.moveTab)
+        self.tabBar.detachedTabDropSignal.connect(self.detachedTabDrop)
+
+        self.setTabBar(self.tabBar)
+
+        # Used to keep a reference to detached tabs since their QMainWindow
+        # does not have a parent
+        self.detachedTabs = {}
+
+        # Close all detached tabs if the application is closed explicitly
+        QtWidgets.qApp.aboutToQuit.connect(self.closeDetachedTabs) # @UndefinedVariable
+
+        self.setTabsClosable(True)
+        self.tabCloseRequested.connect(self.closeTab)
+
+
+    def deleteTab(self, currentIndex):
+        widget = self.widget(currentIndex)
+        if widget is not None:
+            widget.deleteLater()
+        self.removeTab(currentIndex)
+
+    def closeTab(self, currentIndex):
+        self.removeTab(currentIndex)
+
+    def protectTab(self, currentIndex):
+        # self.FCTabBar().setTabButton(currentIndex, QtWidgets.QTabBar.RightSide, None)
+        self.tabBar.setTabButton(currentIndex, QtWidgets.QTabBar.RightSide, None)
+
+    ##
+    #  The default movable functionality of QTabWidget must remain disabled
+    #  so as not to conflict with the added features
+    def setMovable(self, movable):
+        pass
+
+    ##
+    #  Move a tab from one position (index) to another
+    #
+    #  @param    fromIndex    the original index location of the tab
+    #  @param    toIndex      the new index location of the tab
+    @pyqtSlot(int, int)
+    def moveTab(self, fromIndex, toIndex):
+        widget = self.widget(fromIndex)
+        icon = self.tabIcon(fromIndex)
+        text = self.tabText(fromIndex)
+
+        self.removeTab(fromIndex)
+        self.insertTab(toIndex, widget, icon, text)
+        self.setCurrentIndex(toIndex)
+
+
+    ##
+    #  Detach the tab by removing it's contents and placing them in
+    #  a DetachedTab window
+    #
+    #  @param    index    the index location of the tab to be detached
+    #  @param    point    the screen position for creating the new DetachedTab window
+    @pyqtSlot(int, QtCore.QPoint)
+    def detachTab(self, index, point):
+
+        # Get the tab content
+        name = self.tabText(index)
+        icon = self.tabIcon(index)
+        if icon.isNull():
+            icon = self.window().windowIcon()
+        contentWidget = self.widget(index)
+
+        try:
+            contentWidgetRect = contentWidget.frameGeometry()
+        except AttributeError:
+            return
+
+        # Create a new detached tab window
+        detachedTab = self.FCDetachedTab(name, contentWidget)
+        detachedTab.setWindowModality(QtCore.Qt.NonModal)
+        detachedTab.setWindowIcon(icon)
+        detachedTab.setGeometry(contentWidgetRect)
+        detachedTab.onCloseSignal.connect(self.attachTab)
+        detachedTab.onDropSignal.connect(self.tabBar.detachedTabDrop)
+        detachedTab.move(point)
+        detachedTab.show()
+
+
+        # Create a reference to maintain access to the detached tab
+        self.detachedTabs[name] = detachedTab
+
+
+    ##
+    #  Re-attach the tab by removing the content from the DetachedTab window,
+    #  closing it, and placing the content back into the DetachableTabWidget
+    #
+    #  @param    contentWidget    the content widget from the DetachedTab window
+    #  @param    name             the name of the detached tab
+    #  @param    icon             the window icon for the detached tab
+    #  @param    insertAt         insert the re-attached tab at the given index
+    def attachTab(self, contentWidget, name, icon, insertAt=None):
+
+        # Make the content widget a child of this widget
+        contentWidget.setParent(self)
+
+        # Remove the reference
+        del self.detachedTabs[name]
+
+        # Create an image from the given icon (for comparison)
+        if not icon.isNull():
+            try:
+                tabIconPixmap = icon.pixmap(icon.availableSizes()[0])
+                tabIconImage = tabIconPixmap.toImage()
+            except IndexError:
+                tabIconImage = None
+        else:
+            tabIconImage = None
+
+        # Create an image of the main window icon (for comparison)
+        if not icon.isNull():
+            try:
+                windowIconPixmap = self.window().windowIcon().pixmap(icon.availableSizes()[0])
+                windowIconImage = windowIconPixmap.toImage()
+            except IndexError:
+                windowIconImage = None
+        else:
+            windowIconImage = None
+
+        # Determine if the given image and the main window icon are the same.
+        # If they are, then do not add the icon to the tab
+        if tabIconImage == windowIconImage:
+            if insertAt == None:
+                index = self.addTab(contentWidget, name)
+            else:
+                index = self.insertTab(insertAt, contentWidget, name)
+        else:
+            if insertAt == None:
+                index = self.addTab(contentWidget, icon, name)
+            else:
+                index = self.insertTab(insertAt, contentWidget, icon, name)
+
+
+        # Make this tab the current tab
+        if index > -1:
+            self.setCurrentIndex(index)
+
+
+    ##
+    #  Remove the tab with the given name, even if it is detached
+    #
+    #  @param    name    the name of the tab to be removed
+    def removeTabByName(self, name):
+
+        # Remove the tab if it is attached
+        attached = False
+        for index in range(self.count()):
+            if str(name) == str(self.tabText(index)):
+                self.removeTab(index)
+                attached = True
+                break
+
+
+        # If the tab is not attached, close it's window and
+        # remove the reference to it
+        if not attached:
+            for key in self.detachedTabs:
+                if str(name) == str(key):
+                    self.detachedTabs[key].onCloseSignal.disconnect()
+                    self.detachedTabs[key].close()
+                    del self.detachedTabs[key]
+                    break
+
+
+    ##
+    #  Handle dropping of a detached tab inside the DetachableTabWidget
+    #
+    #  @param    name     the name of the detached tab
+    #  @param    index    the index of an existing tab (if the tab bar
+    #                     determined that the drop occurred on an
+    #                     existing tab)
+    #  @param    dropPos  the mouse cursor position when the drop occurred
+    @QtCore.pyqtSlot(str, int, QtCore.QPoint)
+    def detachedTabDrop(self, name, index, dropPos):
+
+        # If the drop occurred on an existing tab, insert the detached
+        # tab at the existing tab's location
+        if index > -1:
+
+            # Create references to the detached tab's content and icon
+            contentWidget = self.detachedTabs[name].contentWidget
+            icon = self.detachedTabs[name].windowIcon()
+
+            # Disconnect the detached tab's onCloseSignal so that it
+            # does not try to re-attach automatically
+            self.detachedTabs[name].onCloseSignal.disconnect()
+
+            # Close the detached
+            self.detachedTabs[name].close()
+
+            # Re-attach the tab at the given index
+            self.attachTab(contentWidget, name, icon, index)
+
+
+        # If the drop did not occur on an existing tab, determine if the drop
+        # occurred in the tab bar area (the area to the side of the QTabBar)
+        else:
+
+            # Find the drop position relative to the DetachableTabWidget
+            tabDropPos = self.mapFromGlobal(dropPos)
+
+            # If the drop position is inside the DetachableTabWidget...
+            if self.rect().contains(tabDropPos):
+
+                # If the drop position is inside the tab bar area (the
+                # area to the side of the QTabBar) or there are not tabs
+                # currently attached...
+                if tabDropPos.y() < self.tabBar.height() or self.count() == 0:
+
+                    # Close the detached tab and allow it to re-attach
+                    # automatically
+                    self.detachedTabs[name].close()
+
+
+    ##
+    #  Close all tabs that are currently detached.
+    def closeDetachedTabs(self):
+        listOfDetachedTabs = []
+
+        for key in self.detachedTabs:
+            listOfDetachedTabs.append(self.detachedTabs[key])
+
+        for detachedTab in listOfDetachedTabs:
+            detachedTab.close()
+
+
+    ##
+    #  When a tab is detached, the contents are placed into this QMainWindow.  The tab
+    #  can be re-attached by closing the dialog or by dragging the window into the tab bar
+    class FCDetachedTab(QtWidgets.QMainWindow):
+        onCloseSignal = pyqtSignal(QtWidgets.QWidget, str, QtGui.QIcon)
+        onDropSignal = pyqtSignal(str, QtCore.QPoint)
+
+        def __init__(self, name, contentWidget):
+            QtWidgets.QMainWindow.__init__(self, None)
+
+            self.setObjectName(name)
+            self.setWindowTitle(name)
+
+            self.contentWidget = contentWidget
+            self.setCentralWidget(self.contentWidget)
+            self.contentWidget.show()
+
+            self.windowDropFilter = self.WindowDropFilter()
+            self.installEventFilter(self.windowDropFilter)
+            self.windowDropFilter.onDropSignal.connect(self.windowDropSlot)
+
+
+        ##
+        #  Handle a window drop event
+        #
+        #  @param    dropPos    the mouse cursor position of the drop
+        @QtCore.pyqtSlot(QtCore.QPoint)
+        def windowDropSlot(self, dropPos):
+            self.onDropSignal.emit(self.objectName(), dropPos)
+
+
+        ##
+        #  If the window is closed, emit the onCloseSignal and give the
+        #  content widget back to the DetachableTabWidget
+        #
+        #  @param    event    a close event
+        def closeEvent(self, event):
+            self.onCloseSignal.emit(self.contentWidget, self.objectName(), self.windowIcon())
+
+
+        ##
+        #  An event filter class to detect a QMainWindow drop event
+        class WindowDropFilter(QtCore.QObject):
+            onDropSignal = pyqtSignal(QtCore.QPoint)
+
+            def __init__(self):
+                QtCore.QObject.__init__(self)
+                self.lastEvent = None
+
+
+            ##
+            #  Detect a QMainWindow drop event by looking for a NonClientAreaMouseMove (173)
+            #  event that immediately follows a Move event
+            #
+            #  @param    obj    the object that generated the event
+            #  @param    event  the current event
+            def eventFilter(self, obj, event):
+
+                # If a NonClientAreaMouseMove (173) event immediately follows a Move event...
+                if self.lastEvent == QtCore.QEvent.Move and event.type() == 173:
+
+                    # Determine the position of the mouse cursor and emit it with the
+                    # onDropSignal
+                    mouseCursor = QtGui.QCursor()
+                    dropPos = mouseCursor.pos()
+                    self.onDropSignal.emit(dropPos)
+                    self.lastEvent = event.type()
+                    return True
+
+                else:
+                    self.lastEvent = event.type()
+                    return False
+
+    class FCTabBar(QtWidgets.QTabBar):
+        onDetachTabSignal = pyqtSignal(int, QtCore.QPoint)
+        onMoveTabSignal = pyqtSignal(int, int)
+        detachedTabDropSignal = pyqtSignal(str, int, QtCore.QPoint)
+
+        def __init__(self, parent=None):
+            QtWidgets.QTabBar.__init__(self, parent)
+
+            self.setAcceptDrops(True)
+            self.setElideMode(QtCore.Qt.ElideRight)
+            self.setSelectionBehaviorOnRemove(QtWidgets.QTabBar.SelectLeftTab)
+
+            self.dragStartPos = QtCore.QPoint()
+            self.dragDropedPos = QtCore.QPoint()
+            self.mouseCursor = QtGui.QCursor()
+            self.dragInitiated = False
+
+
+        #  Send the onDetachTabSignal when a tab is double clicked
+        #
+        #  @param    event    a mouse double click event
+        def mouseDoubleClickEvent(self, event):
+            event.accept()
+            self.onDetachTabSignal.emit(self.tabAt(event.pos()), self.mouseCursor.pos())
+
+
+        #  Set the starting position for a drag event when the mouse button is pressed
+        #
+        #  @param    event    a mouse press event
+        def mousePressEvent(self, event):
+            if event.button() == QtCore.Qt.LeftButton:
+                self.dragStartPos = event.pos()
+
+            self.dragDropedPos.setX(0)
+            self.dragDropedPos.setY(0)
+
+            self.dragInitiated = False
+
+            QtWidgets.QTabBar.mousePressEvent(self, event)
+
+
+        #  Determine if the current movement is a drag.  If it is, convert it into a QDrag.  If the
+        #  drag ends inside the tab bar, emit an onMoveTabSignal.  If the drag ends outside the tab
+        #  bar, emit an onDetachTabSignal.
+        #
+        #  @param    event    a mouse move event
+        def mouseMoveEvent(self, event):
+
+            # Determine if the current movement is detected as a drag
+            if not self.dragStartPos.isNull() and ((event.pos() - self.dragStartPos).manhattanLength() < QtWidgets.QApplication.startDragDistance()):
+                self.dragInitiated = True
+
+            # If the current movement is a drag initiated by the left button
+            if (((event.buttons() & QtCore.Qt.LeftButton)) and self.dragInitiated):
+
+                # Stop the move event
+                finishMoveEvent = QtGui.QMouseEvent(QtCore.QEvent.MouseMove, event.pos(), QtCore.Qt.NoButton, QtCore.Qt.NoButton, QtCore.Qt.NoModifier)
+                QtWidgets.QTabBar.mouseMoveEvent(self, finishMoveEvent)
+
+                # Convert the move event into a drag
+                drag = QtGui.QDrag(self)
+                mimeData = QtCore.QMimeData()
+                # mimeData.setData('action', 'application/tab-detach')
+                drag.setMimeData(mimeData)
+                # screen = QScreen(self.parentWidget().currentWidget().winId())
+                # Create the appearance of dragging the tab content
+                pixmap = self.parent().widget(self.tabAt(self.dragStartPos)).grab()
+                targetPixmap = QtGui.QPixmap(pixmap.size())
+                targetPixmap.fill(QtCore.Qt.transparent)
+                painter = QtGui.QPainter(targetPixmap)
+                painter.setOpacity(0.85)
+                painter.drawPixmap(0, 0, pixmap)
+                painter.end()
+                drag.setPixmap(targetPixmap)
+
+                # Initiate the drag
+                dropAction = drag.exec_(QtCore.Qt.MoveAction | QtCore.Qt.CopyAction)
+
+
+                # For Linux:  Here, drag.exec_() will not return MoveAction on Linux.  So it
+                #             must be set manually
+                if self.dragDropedPos.x() != 0 and self.dragDropedPos.y() != 0:
+                    dropAction = QtCore.Qt.MoveAction
+
+
+                # If the drag completed outside of the tab bar, detach the tab and move
+                # the content to the current cursor position
+                if dropAction == QtCore.Qt.IgnoreAction:
+                    event.accept()
+                    self.onDetachTabSignal.emit(self.tabAt(self.dragStartPos), self.mouseCursor.pos())
+
+                # Else if the drag completed inside the tab bar, move the selected tab to the new position
+                elif dropAction == QtCore.Qt.MoveAction:
+                    if not self.dragDropedPos.isNull():
+                        event.accept()
+                        self.onMoveTabSignal.emit(self.tabAt(self.dragStartPos), self.tabAt(self.dragDropedPos))
+            else:
+                QtWidgets.QTabBar.mouseMoveEvent(self, event)
+
+        #  Determine if the drag has entered a tab position from another tab position
+        #
+        #  @param    event    a drag enter event
+        def dragEnterEvent(self, event):
+            mimeData = event.mimeData()
+            # formats = mcd imeData.formats()
+
+        # if formats.contains('action') and mimeData.data('action') == 'application/tab-detach':
+        # event.acceptProposedAction()
+
+            QtWidgets.QTabBar.dragMoveEvent(self, event)
+
+        #  Get the position of the end of the drag
+        #
+        #  @param    event    a drop event
+        def dropEvent(self, event):
+            self.dragDropedPos = event.pos()
+            QtWidgets.QTabBar.dropEvent(self, event)
+
+
+
+        #  Determine if the detached tab drop event occurred on an existing tab,
+        #  then send the event to the DetachableTabWidget
+        def detachedTabDrop(self, name, dropPos):
+
+            tabDropPos = self.mapFromGlobal(dropPos)
+
+            index = self.tabAt(tabDropPos)
+
+            self.detachedTabDropSignal.emit(name, index, dropPos)
+
+
 class VerticalScrollArea(QtWidgets.QScrollArea):
     """
     This widget extends QtGui.QScrollArea to make a vertical-only

+ 6 - 0
ObjectCollection.py

@@ -273,6 +273,11 @@ class ObjectCollection(QtCore.QAbstractItemModel):
 
             if key == QtCore.Qt.Key_S:
                 self.app.on_file_saveproject()
+
+            # Toggle Plot Area
+            if key == QtCore.Qt.Key_F10:
+                self.app.on_toggle_plotarea()
+
             return
         elif modifiers == QtCore.Qt.ShiftModifier:
 
@@ -324,6 +329,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
             if key == QtCore.Qt.Key_Y:
                 self.app.on_skewy()
                 return
+
         elif modifiers == QtCore.Qt.AltModifier:
             # Eanble all plots
             if key == Qt.Key_1:

+ 2 - 0
README.md

@@ -18,6 +18,8 @@ CAD program, and create G-Code for Isolation routing.
 - updated the camlib.CNCJob.scale() function so now the GCode is scaled also (quite a HACK :( it will need to be replaced at some point)). Units change work now on the GCODE also.
 - added the bounds coordinates to the GCODE header
 - FlatCAM saves now to a file in self.data_path the toolbar positions and the position of TCL Shell
+- Plot Area Tab view can now be toggled, added entry in View Menu and shortcut key CTRL+F10
+- All the tabs in the GUI right side are (Plot Are, Preferences etc) are now detachable to a separate windows which when closed it returns in the previous location in the toolbar. Those detached tabs can be also reattached by drag and drop.
 
 30.01.2019
 

BIN
share/plot32.png