Search This Blog

Tuesday, 18 November 2014

ZyXEL ES-1552 - removing ports from the default VLAN

Edit: As of January 2015 the ES-1552 has been discontinued.

The ZyXEL ES-1552 is a 48 port 10/100 switch that's become horrifically good value recently, at the time of writing Ebuyer has it in stock for £27 (including delivery.) (Sorry, it's now out of stock and discontinued.) Granted, it's not gigabit, but still - a fully managed, fanless 48 port switch at that price? (And actually, it does have 2 gigabit ports included, so it's really a 50 port switch, and 52 if you include the two SFP slots. Anyway, I digress.)

However, one of the downsides I keep seeing mentioned is that the web interface annoyingly doesn't let you remove ports from the default VLAN, even when the PVID of that port is set to a different VLAN! This would make it practically useless in a VLAN setting, but fortunately there's an easy workaround which I'll document here.

I'll be using Chrome here, but any other browser with similar developer functionality should work just as well. For demo purposes, I'll be removing port 4 from the default VLAN. This may seem a bit long-winded, but it's really rather quick once you've done it for the first time.

So without further ado:


  1. Fire up Chrome and plug in the switches' address, and log in.
  2. Make sure you have at least one other VLAN - if you need to, create a second. (This is trivial, just click on "VLAN" in the left hand menu, click "Create new VLAN" and give it an id.)
  3. Click on the "Port" link in the left hand menu, and check the port that you want to remove from the default VLAN has a PVID that's not 1:


    In this case, port 4's PVID is 1, so we need to change it. Click on "04", change the PVID field to "2", and hit "Apply".
  4. Click on the VLAN link in the left, you'll be greeted with something like this:

  5. Right click a VLAN ID that is not 1 (so we right-click on 2 in our case), and select "Open link in new tab". Opening it in a new tab is important! You should see the VLAN table on its own in a tab (not with the normal left and top banner) like the following:

  6. Click on the icon under the port you want to remove from VLAN 1 (port 04 in our case) until it's untagged
  7. Press ctrl+shift+i to open the Chrome developer toolbar.
  8. Hit the "console" button, then type in (exactly as here, no quotes) "cur_vid=1;" and press return:


  9. Close the developer toolbar, then hit "Apply", and that should be it! Now when you click on "VLAN" in the left menu, then "01", you should see that the port has been removed:


    .

Any questions, comments or suggestions regarding the above process then do feel free to leave a comment. It's possible ZyXEL may fix this in an upcoming firmware update so ports can be removed from VLAN 1 without such a hack, but since the last firmware release was a number of years ago this does (unfortunately) seem unlikely.

Still, for a £27 switch I can live with firing up Chrome's developer toolbar and writing one line of Javascript when I need to perform a relatively rare operation!

Thursday, 27 March 2014

Expanding JavaFX's media support

Note: For those that don't want to read through the post and just want the patch for MKV support, you can grab it from this ticket, or here if you don't have a JIRA account.

Background

One of the predominant things lacking a "nice" approach in the Java world for years now has been good media support. Oh sure, we had JMF, but anyone who ever had the misfortune of using that will I'm sure understand why that never really took on. (Yes, it really was that bad.) A few other approaches came and went, most notably Java Media Components - but none ever made there way into core Java, and for a long time it became pretty de-facto knowledge that if you wanted any form of comprehensive media support in Java, you used a cross-platform native library, perhaps with a Java wrapper.

However, when JavaFX 2 came along we were provided with a new, baked in media framework that provided this functionality on the Java level! This is a massive step forward, sure it uses GStreamer underneath but that's not really an issue - the required libraries are baked into JavaFX, and the end user can just treat the corresponding MediaView as any other node in the scenegraph.

However, while this is much better than what we had previously, it's rather limited support at this stage:

7. Does JavaFX provide support for audio and video codecs?

JavaFX provides a common set of APIs that make it easy to include media playback within any JavaFX application. The media formats currently supported are the following:
  • Audio: MP3; AIFF containing uncompressed PCM; WAV containing uncompressed PCM; MPEG-4 multimedia container with Advanced Audio Coding (AAC) audio
  • Video: FLV containing VP6 video and MP3 audio; MPEG-4 multimedia container with H.264/AVC (Advanced Video Coding) video compression .

The format support is far from useless; for videos bundled with an application that can be in any format, mp4 with h264 and AAC is certainly a relatively standard option. Likewise, if you're writing an application that uploads videos and uses something like ffmpeg to convert them over to a standard format before displaying them with a JFX frontend somehow, this is also adequate.

However, there are many use cases where the current support is very restrictive indeed - certainly any general purpose media player is out of the question, as is (realistically) any application where you want user-selected video files to play with any degree of reliability. Two of the most common container formats are out whatever formats are inside them (MKV and the badly ageing AVI), AC3 audio support isn't there... well, anything that isn't in the above list isn't there. Which is a lot. Many of these will have been excluded for licensing reasons, though even many free ones that could be in there aren't (MKV, OGG, FLAC, etc.)

But as said already, the JavaFX media classes use GStreamer to do the heavy lifting, which has all these formats (and more) available to it through plugins. So now that the whole thing is open sourced, it should in theory be possible to rebuild JFX with more GStreamer plugins compiled in, right? Turns out it is - the following is an outline of the process of how I did it. I'll be adding support for the MKV container here, other plugins can no doubt be added in a similar way.

Note: I'm far from an authority on this subject, I'm not a JFX developer and I'm certainly not advocating that what I describe here is necessarily all accurate, or correct. It's merely what I've been able to work out from tracing through the source and from some helpful people on the openjfx-dev mailing list.

Setting up the build

You'll firstly need to check out the JFX repo using Mercurial:

hg clone http://hg.openjdk.java.net/openjfx/8u-dev/master/
You will of course need a Mercurial client. If you're using Windows and haven't got a Mercurial client installed already, I highly recommend TortoiseHg. The clone may take a while to complete (the repository is on the larger side!) so be patient.

When you've checked out the repository, you'll then need to make sure you have all the prerequisites you need to build successfully - for that, see the build page and make sure you have everything installed that you need (for Windows users, you must have Cygwin installed with the listed plugins.)

There's a couple of points you also must take note of if you're running Windows that aren't mentioned in the document however:

  • If the DirectX SDK fails to install then it's almost definitely because of the issue described here - uninstall the relevant packages, then try the install again and it should go through without an issue.
  • You must also have the samples from the Windows SDK installed.
  • We want to compile the media module, which is disabled by default for timing reasons. In the root of the repo, copy gradle.properties.template to gradle.properties, then uncomment the line (delete the first hash) that says "#COMPILE_MEDIA = true".
  • Things will be much easier if you add your gradle bin folder to PATH.
You can then fire up a shell, type "gradle sdk" and watch it attempt to build. Since you haven't made any changes at this point, all should go well and you should be presented with a "BUILD SUCCESSFUL" message after a while (the build will take a few minutes to complete.) If not, then go back and double check you've set everything up as per the instructions on the build page. Of course, if you're still stuck then feel free to leave a comment :)

Making the changes

At this point, there's two ways you can proceed - the first is to just grab the patch file, apply it to the repo, and then rebuild (run "gradle sdk" again.) All being well, the MKV container will then be supported in the resulting build. So if you want to just do that the easy way, you can skip the rest of this step.

However, in the interest of being as informative as possible to those that want to repeat the step with another plugin, I'll describe the necessary changes here in detail. Changes on both the Java and native layer are required, so we'll start with the Java layer.

Java layer

The bit of the Java layer that we're interested in is really just responsible for performing some basic checks on the file's type to determine if it has a hope of playing it. This is a relatively simple process, hopefully explained by the diagram below:


This means we have to add support in two places; we have to modify the filenameToContentType() method so it can work out the correct content type from the file name, and we then have to add it to the list of supported content types for the corresponding platform, GSTPlatform in this case (this is just an entry in the array.) Optionally we could also add knowledge of the signature to the fileSignatureToContentType() method, but that isn't strictly necessary since it's just used as a fallback if the type can't be worked out from the fileNameToContentType() method. In doing so you'll also need to add the extension and content type to the MediaUtils class:

public static final String CONTENT_TYPE_MKV = "video/x-matroska";
private static final String FILE_TYPE_MKV = "mkv";

The file extension is obvious, but the content type should be grabbed from the GStreamer defined types list here.

I won't go into huge amounts of detail on what exactly to change in the Java classes - it's pretty basic Java, and I'm assuming most of the people reading this will be Java programmers! You can of course look at the patch to see my exact changes. If you get stuck, feel free to leave a comment and I'll do my best to help.

When you've done this, rebuild JFX (run "gradle sdk") and try creating a media object to point to an MKV file. If the above has worked successfully, you'll get a different (native) error to the one you got before. This is good - it means the Java layer is letting the file pass down to be played in the native layer - now we need to enable it here.

Native Layer

We now need to grab the required plugin for GStreamer - in the case of the matroska plugin, this is in the "plugins-good" category, which means it's a well written and tested plugin that shouldn't pose distribution problems. The tarball of source for the plugins can be grabbed from here.

However, make sure when you're doing this that you grab the correct version of the plugins - JavaFX (for 8u20 and before at least) isn't built with the latest GStreamer. To find out what one, we can look at the GStreamer modifications that Oracle publish, a zip file containing modifications to the bits of GStreamer they've included. This shows the plugins being used are the 0.10.30 ones (for the good branch anyway), so go ahead, download the 0.10.30 good plugins and pull out the matroska one, and drop it in the relevant directory - in this case "modules/media/src/main/native/gstreamer/gstreamer-lite/gst-plugins-good/gst/".

Most of the modifications that Oracle make are fixes and performance improvements to the other plugins, though one required change is to the plugin loading system. In the main plugin C file, matroska.c in this case, you'll see an init function like the following:

GST_PLUGIN_DEFINE (GST_VERSION_MAJOR,
    GST_VERSION_MINOR,
    "matroska",
    "Matroska and WebM stream handling",
    plugin_init, VERSION, "LGPL", GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN)

We don't need this function, so we can remove it - instead of removing it though, make sure you follow the convention of wrapping it in "#ifndef":

+#ifndef GSTREAMER_LITE
+GST_PLUGIN_DEFINE (GST_VERSION_MAJOR,
+    GST_VERSION_MINOR,
+    "matroska",
+    "Matroska and WebM stream handling",
+    plugin_init, VERSION, "LGPL", GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN)
+#endif

This then makes it much easier to find where changes have been made later on, as well as preserving the original functionality if it's not being built in the JFX environment for whatever reason (in which case GSTREAMER_LITE won't be defined, so the above will execute.)

We also need to change the plugin_init function in a similar way, which is the following:

static gboolean
plugin_init (GstPlugin * plugin)

We want to make two changes here - we don't want it to be static, and we want the name to be more unique so it can be initialised alongside other plugins without any conflict. The convention appears to be "plugin_init_pluginname", so replace the above with the following:

#ifdef GSTREAMER_LITE
gboolean
plugin_init_matroska (GstPlugin * plugin)
#else // GSTREAMER_LITE
static gboolean
plugin_init (GstPlugin * plugin)
#endif // GSTREAMER_LITE

Again, the changes are wrapped in the appropriate tags to make it clear what we've changed.

That's all the changes for this file, but we need to add the method we've defined (plugin_init_matroska in this case) to the appropriate headers file, "modules/media/src/main/native/gstreamer/gstreamer-lite/projects/plugins/gstplugins-lite.h". Open it, and add:

gboolean plugin_init_matroska (GstPlugin * plugin);

...or whatever you called your function above to the list of method headers. We also need to make sure the function is called to initialise the plugin, so open up modules/media/src/main/native/gstreamer/gstreamer-lite/projects/plugins/gstplugins-lite.c and find the section where the plugins are initialised, it'll be a list in a big if statement looking something like this:
...
!plugin_init_aiff(plugin) ||
!plugin_init_app(plugin) ||
!plugin_init_audioparsers(plugin) ||
...
Then just add the call to your plugin in the same way:
...
!plugin_init_aiff(plugin) ||
!plugin_init_app(plugin) ||
!plugin_init_audioparsers(plugin) ||
!plugin_init_matroska(plugin) ||
...
We now need to update the JavaFX cpp bridging code to make it aware of the format, and create and return a pipeline for it. Let's start by defining the format in modules/media/src/main/native/jfxmedia/MediaManagement/MediaTypes.h - this is just a case of adding it to the list, making sure it's the same as you defined it in the Java code. So for this case, the following needs to be added:

#define CONTENT_TYPE_MKV    "video/x-matroska"
Next, open modules/media/src/main/native/jfxmedia/platform/gstreamer/GstPipelineFactory.cpp - this is where the pipeline creation actually takes place. There's a few things that need to be changed in here:
  1. Find the pushback function calls, something like:
         m_ContentTypes.push_back(CONTENT_TYPE_MP4);
         m_ContentTypes.push_back(CONTENT_TYPE_M4A);
         m_ContentTypes.push_back(CONTENT_TYPE_M4V);
    Then add your content type to it in the same way:
         m_ContentTypes.push_back(CONTENT_TYPE_MKV);
  2. The next step is a bit less clear cut, you need to find the CreatePlayerPipeline function, and identify the part where your pipeline should be created. You'll see a general pattern here - the video container formats are dealt with within one if statement that sets up a video sink before creating the pipeline, and the audio formats are dealt with afterwards. So in the appropriate place, you need to follow the pattern to hook in and call a method to create your pipeline. For MKV, it seemed to make most sense to add it to the if/else block just after checking for mp4 files, so straight afterwards I added this:
    
        else if (CONTENT_TYPE_MKV == locator->GetContentType())
     {
      uRetCode = CreateMKVPipeline(pSource, pVideoSink, (CPipelineOptions*) pOptions, ppPipeline);
      if (ERROR_NONE != uRetCode)
                   return uRetCode;
            }
  3. Of course, you then need to write the function to return the pipeline, and again you can follow the pattern of similar ones for this. Once again, MKV is similar in the way it's handled to MP4 so I simply copied that function and made the appropriate changes thus producing:
    1. uint32_t CGstPipelineFactory::CreateMKVPipeline(GstElement* source, GstElement* pVideoSink, CPipelineOptions* pOptions, CPipeline** ppPipeline)
      {
      #if TARGET_OS_WIN32
          return CreateAVPipeline(source, "matroskademux", "dshowwrapper", true, dshowwrapper", pVideoSink, pOptions, ppPipeline);
      #elif TARGET_OS_MAC
          return CreateAVPipeline(source, "matroskademux", "audioconverter", false, avcdecoder", pVideoSink, pOptions, ppPipeline);
      #elif TARGET_OS_LINUX
      #if ENABLE_GST_FFMPEG
          return CreateAVPipeline(source, "matroskademux", "ffdec_aac", true,
                                  "ffdec_h264", pVideoSink, pOptions, ppPipeline);
      #else // ENABLE_GST_FFMPEG
          return CreateAVPipeline(source, "matroskademux", "avaudiodecoder", false, "avvideodecoder", pVideoSink, pOptions, ppPipeline);
      #endif // ENABLE_GST_FFMPEG
      #else
          return ERROR_PLATFORM_UNSUPPORTED;
      #endif // TARGET_OS_WIN32
      }
    The only things I changed from the CreateMP4Pipeline were the name of the function, and the name of the demuxing plugin (matroskademux in this case) - everything else remains the same.
  4. Of course, you'll now need to add the above function to the relevant header file, so in modules/media/src/main/native/jfxmedia/platform/gstreamer/GstPipelineFactory.h, add:
    
    
    uint32_t    CreateMKVPipeline(GstElement* source, GstElement* videosink, CPipelineOptions* pOptions, CPipeline** ppPipeline);
    
    
    ...to the list of functions.
That should be all the changes you need to make to the native code, now we just need to ensure it's compiled and linked properly. So to start with, you'll need to update the plugins makefile, modules/media/src/main/native/gstreamer/projects/win/gstreamer-lite/Makefile.gstplugins - add the directory and all the c files in that appropriate directory. So my list of directories now looks (partly) something like:
...
gst-plugins-good/gst/spectrum/ \
gst-plugins-good/gst/wavparse/ \
gst-plugins-good/gst/matroska/ \
gstreamer/plugins/elements/ \
gstreamer/plugins/indexers/ \
...

And the list of files:

...
gst-plugins-good/gst/spectrum/gstspectrum.c \
gst-plugins-good/gst/wavparse/gstwavparse.c \
gst-plugins-good/gst/matroska/webm-mux.c \
gst-plugins-good/gst/matroska/matroska-parse.c \
gst-plugins-good/gst/matroska/matroska-mux.c \
gst-plugins-good/gst/matroska/matroska-ids.c \
gst-plugins-good/gst/matroska/matroska-demux.c \
gst-plugins-good/gst/matroska/matroska.c \
gst-plugins-good/gst/matroska/lzo.c \
gst-plugins-good/gst/matroska/ebml-write.c \
gst-plugins-good/gst/matroska/ebml-read.c \
gstreamer/plugins/elements/gstcapsfilter.c \
gstreamer/plugins/elements/gstelements.c \
...

The boldings are my addition (obviously the above is just an excerpt.)

Now you can try and build and see if you encounter any compilation errors (you could and arguably should of course, do this as you're going through as well.) I found odd things sometimes cropped up if I didn't do a clean (gradle clean) before re-building, so if something doesn't seem quite right, bear that in mind.

You shouldn't have any compilation errors at this point, but you almost certainly will have linker errors (probably starting with "Unresolved external symbol" or something similar, then referencing a function.) These functions will either start with g_ or gst_ (ignore the leading underscore.) The ones that start with gst need to be added to modules/media/src/main/native/gstreamer/projects/win/gstreamer-lite.def matching the format of the file already there. This means for each function, you need a new line in the format of:

function_name<TAB>@nextsequentialnumber<TAB>NONAME.

Obviously, function_name should be replaced with the function name, <TAB> should be replaced with an actual tab and nextsequentialnumber should be replaced with, you guessed it, the next sequential number. Apart from that you don't need to worry about ordering. For the matroska plugin, I had to add the following to the end of the file:

gst_byte_writer_free_and_get_buffer @184 NONAME
gst_byte_writer_free @185 NONAME
gst_byte_writer_new_with_size @186 NONAME

For the functions that start with "g_" instead, you need to add these in the exact same way to both the modules/media/src/main/native/gstreamer/3rd_party/glib/glib-2.28.8/build/win32/vs100/glib-lite.def and the modules/media/src/main/native/gstreamer/3rd_party/glib/glib-2.28.8/build/win32/vs100/glib-liteD.def files.

If you recompile and it now complains about an unresolved external in one of the def files itself, changes are the source that contains those functions isn't actually getting compiled (because it wasn't needed in any of the plugins already there.) For the Matroska plugin this is the case for the bytewriter functions mentioned above. These are in gstbytewriter.c, and a quick check in the makefile (modules/media/src/main/native/gstreamer/projects/win/gstreamer-lite/Makefile.gstreamer) indeed confirmed that it wasn't on the list, so that was promptly added:

gstreamer/libs/gst/base/gstbasetransform.c \
gstreamer/libs/gst/base/gstbytereader.c \
gstreamer/libs/gst/base/gstbytewriter.c \
gstreamer/libs/gst/base/gstcollectpads.c \
gstreamer/libs/gst/base/gstpushsrc.c \

And that's it - after performing those steps, doing a full build, then running against the built dll's and jfxrt.jar, media support for MKV (or whatever other format you've chosen) should just work!

Summary

These are relatively extensive instructions, but none are really major changes. Summarised, they're pretty much as follows:

  1. Edit the fileNameToContentType() method to return the correct content type for your file name.
  2. Optionally edit the fileSignatureToContentType() method to return the correct type for your file signature (this page may be of use here.)
  3. Add the content type to the list of GSTPlatform's supported content types
  4. Download the plugin file and drop it in the relevant directory
  5. In the plugin's main file, rename the plugin_init function to something more unique, remove the static modifier
  6. Initialise the plugin in gstplugins-lite.c
  7. Define the media type in MediaTypes.h
  8. In GSTPipelineFactory.cpp, call m_ContentTypes.push_back on the content type
  9. In the same file, in the CreatePlayerPipeline function, add in a hook to call a function to create the pipeline for your content type
  10. Create the above function (using the other ones in that file as a template)
  11. Add the relevant files / directory to Makefile.gstplugins
  12. Make sure all the files you rely on are being built, add any that aren't to Makefile.gstreamer
  13. Add any gstreamer functions (gst) to gstreamer-lite.def
  14. Add any glib functions (g) to glib-lite.def and glib-liteD.def
As said already, the above guide is my (little) experience simply with playing around and adding MKV support, so I can't guarantee it will be exactly the same for other plugins and formats. However, it should at least serve as a starting guide for those wanting to build much more comprehensive media support into JavaFX. There's many, many gstreamer plugins available, most of which will, in all likelihood never be included in JavaFX core because of licensing issues. But if you want to build your own version of JFX to distribute with your application, then building much more media support into it, as described above, is more than do-able.

Friday, 31 January 2014

Draggable and detachable tabs in JavaFX 2

JavaFX currently doesn't have the built in ability to change the order of tabs by dragging them, neither does it have the ability to detach tabs into separate windows (like a lot of browsers do these days.) There is a general issue for improving TabPanes filed here, so if you'd like to see this sort of behaviour added in the main JavaFX libraries then go ahead and cast your vote, it would be a very welcome addition!

However, as nice as this would be in the future, it's not here at the moment and it looks highly unlikely it'll be here for Java 8 either. I've seen a few brief attempts at reordering tabs in JavaFX, but very few examples on dragging them and nothing to do with detaching / reattaching them from the pane.

Given this, I've decided to create a reusable class that should hopefully be as easy as possible to integrate into existing applciations - it extends from Tab, and for the most part you create it and use it like a normal tab (you can just add it to a normal TabPane for instance.) It works pretty well for me as you can see in this simple example:



There's a few things to be aware of however before you rush out and use it!
  • To set the text, make sure you use setLabelText() rather than setText(), otherwise you'll get odd results. Sadly the latter is final so I can't override it.
  • You can't have DraggableTabs and normal Tabs on the same TabPane, otherwise you'll see all sorts of errors pop up. If you don't want a particular tab to be detachable, just call setDetachable(false). Tab 1 (the black tab) is set this way in the example program below.
  • This seems to work well for me, but it's far from bulletproof - use at your own risk! It should be pretty easy to work out what's going on though so if you want to change it, modify or otherwise extend it then redistribute it, feel free.


If you don't want to copy / paste from here, just grab the raw files.

/**
 * Just a very simple sample application that uses the class below.
 */
public class FXTabs extends Application {

    @Override
    public void start(final Stage primaryStage) {

        DraggableTab tab1 = new DraggableTab("Tab 1");
        tab1.setClosable(false);
        tab1.setDetachable(false);
        tab1.setContent(new Rectangle(500, 500, Color.BLACK));
        DraggableTab tab2 = new DraggableTab("Tab 2");
        tab2.setClosable(false);
        tab2.setContent(new Rectangle(500, 500, Color.RED));
        DraggableTab tab3 = new DraggableTab("Tab 3");
        tab3.setClosable(false);
        tab3.setContent(new Rectangle(500, 500, Color.BLUE));
        DraggableTab tab4 = new DraggableTab("Tab 4");
        tab4.setClosable(false);
        tab4.setContent(new Rectangle(500, 500, Color.ORANGE));
        TabPane tabs = new TabPane();
        tabs.getTabs().add(tab1);
        tabs.getTabs().add(tab2);
        tabs.getTabs().add(tab3);
        tabs.getTabs().add(tab4);

        StackPane root = new StackPane();
        root.getChildren().add(tabs);

        Scene scene = new Scene(root);

        primaryStage.setScene(scene);
        primaryStage.show();

    }
}


All the real work happens in this class - it's not the neatest thing in the world by a long stretch, but I've kept it all in one place to make it easier to just copy across and experiment with:

import java.util.HashSet;
import java.util.Set;
import javafx.collections.ListChangeListener;
import javafx.event.EventHandler;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.geometry.Rectangle2D;
import javafx.scene.Scene;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.stage.WindowEvent;

/**
 * A draggable tab that can optionally be detached from its tab pane and shown
 * in a separate window. This can be added to any normal TabPane, however a
 * TabPane with draggable tabs must *only* have DraggableTabs, normal tabs and
 * DrragableTabs mixed will cause issues!
 * <p>
 * @author Michael Berry
 */
public class DraggableTab extends Tab {

    private static final Set<TabPane> tabPanes = new HashSet<>();
    private Label nameLabel;
    private Text dragText;
    private static final Stage markerStage;
    private Stage dragStage;
    private boolean detachable;

    static {
        markerStage = new Stage();
        markerStage.initStyle(StageStyle.UNDECORATED);
        Rectangle dummy = new Rectangle(3, 10, Color.web("#555555"));
        StackPane markerStack = new StackPane();
        markerStack.getChildren().add(dummy);
        markerStage.setScene(new Scene(markerStack));
    }

    /**
     * Create a new draggable tab. This can be added to any normal TabPane,
     * however a TabPane with draggable tabs must *only* have DraggableTabs,
     * normal tabs and DrragableTabs mixed will cause issues!
     * <p>
     * @param text the text to appear on the tag label.
     */
    public DraggableTab(String text) {
        nameLabel = new Label(text);
        setGraphic(nameLabel);
        detachable = true;
        dragStage = new Stage();
        dragStage.initStyle(StageStyle.UNDECORATED);
        StackPane dragStagePane = new StackPane();
        dragStagePane.setStyle("-fx-background-color:#DDDDDD;");
        dragText = new Text(text);
        StackPane.setAlignment(dragText, Pos.CENTER);
        dragStagePane.getChildren().add(dragText);
        dragStage.setScene(new Scene(dragStagePane));
        nameLabel.setOnMouseDragged(new EventHandler<MouseEvent>() {

            @Override
            public void handle(MouseEvent t) {
                dragStage.setWidth(nameLabel.getWidth() + 10);
                dragStage.setHeight(nameLabel.getHeight() + 10);
                dragStage.setX(t.getScreenX());
                dragStage.setY(t.getScreenY());
                dragStage.show();
                Point2D screenPoint = new Point2D(t.getScreenX(), t.getScreenY());
                tabPanes.add(getTabPane());
                InsertData data = getInsertData(screenPoint);
                if(data == null || data.getInsertPane().getTabs().isEmpty()) {
                    markerStage.hide();
                }
                else {
                    int index = data.getIndex();
                    boolean end = false;
                    if(index == data.getInsertPane().getTabs().size()) {
                        end = true;
                        index--;
                    }
                    Rectangle2D rect = getAbsoluteRect(data.getInsertPane().getTabs().get(index));
                    if(end) {
                        markerStage.setX(rect.getMaxX() + 13);
                    }
                    else {
                        markerStage.setX(rect.getMinX());
                    }
                    markerStage.setY(rect.getMaxY() + 10);
                    markerStage.show();
                }
            }
        });
        nameLabel.setOnMouseReleased(new EventHandler<MouseEvent>() {

            @Override
            public void handle(MouseEvent t) {
                markerStage.hide();
                dragStage.hide();
                if(!t.isStillSincePress()) {
                    Point2D screenPoint = new Point2D(t.getScreenX(), t.getScreenY());
                    TabPane oldTabPane = getTabPane();
                    int oldIndex = oldTabPane.getTabs().indexOf(DraggableTab.this);
                    tabPanes.add(oldTabPane);
                    InsertData insertData = getInsertData(screenPoint);
                    if(insertData != null) {
                        int addIndex = insertData.getIndex();
                        if(oldTabPane == insertData.getInsertPane() && oldTabPane.getTabs().size() == 1) {
                            return;
                        }
                        oldTabPane.getTabs().remove(DraggableTab.this);
                        if(oldIndex < addIndex && oldTabPane == insertData.getInsertPane()) {
                            addIndex--;
                        }
                        if(addIndex > insertData.getInsertPane().getTabs().size()) {
                            addIndex = insertData.getInsertPane().getTabs().size();
                        }
                        insertData.getInsertPane().getTabs().add(addIndex, DraggableTab.this);
                        insertData.getInsertPane().selectionModelProperty().get().select(addIndex);
                        return;
                    }
                    if(!detachable) {
                        return;
                    }
                    final Stage newStage = new Stage();
                    final TabPane pane = new TabPane();
                    tabPanes.add(pane);
                    newStage.setOnHiding(new EventHandler<WindowEvent>() {

                        @Override
                        public void handle(WindowEvent t) {
                            tabPanes.remove(pane);
                        }
                    });
                    getTabPane().getTabs().remove(DraggableTab.this);
                    pane.getTabs().add(DraggableTab.this);
                    pane.getTabs().addListener(new ListChangeListener<Tab>() {

                        @Override
                        public void onChanged(ListChangeListener.Change<? extends Tab> change) {
                            if(pane.getTabs().isEmpty()) {
                                newStage.hide();
                            }
                        }
                    });
                    newStage.setScene(new Scene(pane));
                    newStage.initStyle(StageStyle.UTILITY);
                    newStage.setX(t.getScreenX());
                    newStage.setY(t.getScreenY());
                    newStage.show();
                    pane.requestLayout();
                    pane.requestFocus();
                }
            }

        });
    }

    /**
     * Set whether it's possible to detach the tab from its pane and move it to
     * another pane or another window. Defaults to true.
     * <p>
     * @param detachable true if the tab should be detachable, false otherwise.
     */
    public void setDetachable(boolean detachable) {
        this.detachable = detachable;
    }

    /**
     * Set the label text on this draggable tab. This must be used instead of
     * setText() to set the label, otherwise weird side effects will result!
     * <p>
     * @param text the label text for this tab.
     */
    public void setLabelText(String text) {
        nameLabel.setText(text);
        dragText.setText(text);
    }

    private InsertData getInsertData(Point2D screenPoint) {
        for(TabPane tabPane : tabPanes) {
            Rectangle2D tabAbsolute = getAbsoluteRect(tabPane);
            if(tabAbsolute.contains(screenPoint)) {
                int tabInsertIndex = 0;
                if(!tabPane.getTabs().isEmpty()) {
                    Rectangle2D firstTabRect = getAbsoluteRect(tabPane.getTabs().get(0));
                    if(firstTabRect.getMaxY()+60 < screenPoint.getY() || firstTabRect.getMinY() > screenPoint.getY()) {
                        return null;
                    }
                    Rectangle2D lastTabRect = getAbsoluteRect(tabPane.getTabs().get(tabPane.getTabs().size() - 1));
                    if(screenPoint.getX() < (firstTabRect.getMinX() + firstTabRect.getWidth() / 2)) {
                        tabInsertIndex = 0;
                    }
                    else if(screenPoint.getX() > (lastTabRect.getMaxX() - lastTabRect.getWidth() / 2)) {
                        tabInsertIndex = tabPane.getTabs().size();
                    }
                    else {
                        for(int i = 0; i < tabPane.getTabs().size() - 1; i++) {
                            Tab leftTab = tabPane.getTabs().get(i);
                            Tab rightTab = tabPane.getTabs().get(i + 1);
                            if(leftTab instanceof DraggableTab && rightTab instanceof DraggableTab) {
                                Rectangle2D leftTabRect = getAbsoluteRect(leftTab);
                                Rectangle2D rightTabRect = getAbsoluteRect(rightTab);
                                if(betweenX(leftTabRect, rightTabRect, screenPoint.getX())) {
                                    tabInsertIndex = i + 1;
                                    break;
                                }
                            }
                        }
                    }
                }
                return new InsertData(tabInsertIndex, tabPane);
            }
        }
        return null;
    }

    private Rectangle2D getAbsoluteRect(Control node) {
        return new Rectangle2D(node.localToScene(node.getLayoutBounds().getMinX(), node.getLayoutBounds().getMinY()).getX() + node.getScene().getWindow().getX(),
                node.localToScene(node.getLayoutBounds().getMinX(), node.getLayoutBounds().getMinY()).getY() + node.getScene().getWindow().getY(),
                node.getWidth(),
                node.getHeight());
    }

    private Rectangle2D getAbsoluteRect(Tab tab) {
        Control node = ((DraggableTab) tab).getLabel();
        return getAbsoluteRect(node);
    }

    private Label getLabel() {
        return nameLabel;
    }

    private boolean betweenX(Rectangle2D r1, Rectangle2D r2, double xPoint) {
        double lowerBound = r1.getMinX() + r1.getWidth() / 2;
        double upperBound = r2.getMaxX() - r2.getWidth() / 2;
        return xPoint >= lowerBound && xPoint <= upperBound;
    }

    private static class InsertData {

        private final int index;
        private final TabPane insertPane;

        public InsertData(int index, TabPane insertPane) {
            this.index = index;
            this.insertPane = insertPane;
        }

        public int getIndex() {
            return index;
        }

        public TabPane getInsertPane() {
            return insertPane;
        }

    }
}