Category: Technical

Book Review: Scrum: The Art of Doing Twice the Work in Half the Time by Jeff Sutherland

scrum coverI first came in contact with Scrum when I was working at Google and since then, I’ve been applying it to the startups I co-founded with good outcomes. Since I was searching for a job, I kept seeing “Scrum Master” come up over and over and I thought it was about time that I learned all the details of Scrum to be able to be a proper Scrum master. Well, in only a couple of ways I finished the book and discovered I was already a proper Scrum master, having learned all the details about it from my time at Google and blog posts.

About the book itself, it’s short and entertaining with enough story telling to keep you engaged even if you only have a passing interest in Scrum. The system is rather simple, with only a few moving pieces and I’m glad of that. Simple systems tend to work better. The testimonials of how much productive a team is with Scrum feel exaggerated completely out of proportion, but then again, some companies are so terrible at producing anything at all, being the cradle of dysfunction, that is no surprise their productivity can be doubled or quadrupled.

★★★★☆

Buy The Art of Doing Twice the Work in Half the Time in USA

Buy The Art of Doing Twice the Work in Half the Time in UK

Advertisements

Weird interaction with Google, was it Duplex?

For a few weeks I’ve been receiving this email from Google:

birth control

My first question is why is birth control an issue. Is Google limiting the advertisement of birth control? Why? Did we somehow slipped into the 19th century and nobody told me?

My second question was… how could they think I have anything to do with birth control. I guess that was nothing more than AI failing miserably, so, I decided to go and fix it. The important caveat here is that I haven’t run any ads in months, maybe more than a year.

I click the Fix button and and it took me to a black page with the “Unknown Business” title. There was no way to text support, so, when the emails got annoying enough, I called them.

The support guy was nice, but he couldn’t do much about it. I explained that I wasn’t running any ads and I didn’t plan on running any for now, but maybe in the future. He told me to ignore the emails.  I asked if there was a way to stop them and he told me to search for an unsubscribe link at the bottom of the email. That’s not what I meant, I want to fix the problem. All right, that was enough time on the phone, I’ll just ignore the emails.

Now is when it got weirder. The support guy said that his supervisor was there and wanted to talk to me. Ok… Click! Someone else starts speaking:

Supervisor: Hello Hos (their way of pronouncing José), did so-and-so answer your query today?

I’m always careful here. He didn’t solve the problem but I’m sure it wasn’t his fault. Most of the times I have an issue, it’s their system being broken and a support specialist shouldn’t be punished for that.

Me: Sort-of…

And as I was trying to explain the situation, the supervisor interrupt me:

Supervisor: Ok then. If you have any other questions, feel free to call us between 9 am and 5 pm.

Click! Hung up. Wow… that was rude… and odd. And now I’m thinking, did I just talk to Duplex and it failed at managing my answer?

When it comes to technology such as Duplex, my take is this: it’s going to happen no matter what, fighting it is futile, let’s try to figure out how to make the most out of it. But I have to admit having interacted with what I suspect was Duplex gives me an odd feeling (even if it was it). It makes me want to rebel, it makes me want to test it the next time I call to try to figure out if it’s a human or not. This is obviously useless; the only thing that matters is getting my issue resolved. What concerns me here is that if a technology-loving person such as myself is getting this strong reaction, how will the general population react?

I think we are going to have some interesting growing pains in the next couple of decades.

Book Review: WiX 3.6: A Developer’s Guide to Windows Installer XML by Nick Ramirez

This book was instrumental in me managing to package Dashman. It had pretty much everything I needed and most if not everything I learned from the book worked with the latest WiX 3.X.
One issue that I have with most WiX information out there is that it assumes you are working in a .Net language and using Visual Studio. I understand this is true for most WiX users but it’s not for me, as I was building a Java application. If you are in the same situation as me, I recommend reading chapter 1 and then immediately jumping to chapter 9 that explains how to use all the different parts of WiX from the command line. After that go back to chapter 2 and work yourself over to chapter 8.
I haven’t read a lot past chapter 9 because it gets into a lot of very advanced stuff that I didn’t need. I feel that a lot of computer books these days are just introductions. It’s nice to see one that goes so deep into a subject that I find myself not needing to read it, instead of wondering where’s the next more advanced book.
I wish more computer books were like this (but I do understand why they are not).

★★★★★

Buy WiX 3.6: A Developer’s Guide to Windows Installer XML in USA

Buy WiX 3.6: A Developer’s Guide to Windows Installer XML in UK

Implementing Windows’ Restart Manager in Java

Disclaimer: I don’t know what I’m talking about, I’ve done little Win API (Win32) development and I only have a few years of Java development of which maybe 2 or 3 are developing desktop applications with JavaFX (Dashman being my first fully fledged out JavaFX app).

Disclaimer 2: I have only tested this on my own computer, running Microsoft Windows 10. I hope to soon test it in many others and over time we’ll see whether my solution was correct or not. I’ll update this blog post accordingly (or link to a newer version if necessary).

I started taking the quality of Dashman very seriously and one of the problems I found was that the running instances wouldn’t exit properly during uninstall or upgrades. And as I expected, this turned out into a head-bashing-into-brick-wall task. My solution was for a JavaFX app, but this should work for a Swing or any other kind of apps.

It all started with learning about Windows Restart Manager, something I didn’t know it even existed until a week ago. This is what allows Windows to close applications on uninstall, on reboots, etc. In the Guidelines for Applications, the crucial bit is this:

The Restart Manager queries GUI applications for shutdown by sending a WM_QUERYENDSESSION notification that has the lParam parameter set to ENDSESSION_CLOSEAPP (0x1). Applications should not shut down when they receive a WM_QUERYENDSESSION message because another application may not be ready to shut down. GUI applications should listen for the WM_QUERYENDSESSION message and return a value of TRUE if the application is prepared to shut down and restart. If no application returns a value of FALSE, the Restart Manager sends a WM_ENDSESSION message with the lParam parameter set to ENDSESSION_CLOSEAPP (0x1) and the wparam parameter set to TRUE. Applications should shut down only when they receive the WM_ENDSESSION message. The Restart Manager also sends a WM_CLOSE message for GUI applications that do not shut down on receiving WM_ENDSESSION. If any GUI application responds to a WM_QUERYENDSESSION message by returning a value of FALSE, the shutdown is canceled. However, if the shutdown is forced, the application is terminated regardless.

Simplifying it: when Windows needs your app to close, it will send a message asking if you are ready to close. Your application might respond negatively and then no application will be closed. This could happen for example if there’s some unsaved work and the app needs the consent from the user to either save or discard. This is what happens when you try to shut down your computer and Microsoft Word stops it asking whether you want to save the file or not.

After that your application can receive a message asking it to please close or telling it to close now. I’m not sure what the nuances are between these two. For Dashman I decided to just save the config and close in either of these instances.

Receiving these messages requires interfacing with Windows DLLs, for which I’m using JNA. I don’t know how JNA works, I read the code, sort-of understood it, copied and pasted it. What I think is going on is that you open the user32.dll like this:

User32 user32 = Native.loadLibrary("user32", User32.class, Collections.unmodifiableMap(options))

User32 is an interface that contains all the methods with the proper signatures to be able to call them from Java. options just makes sure we are using the Unicode version of the Win32 API calls. You can see that and all the other missing pieces on the full example at the end of the blog post.

I need a Win32 API callback that will receive the messages and actually implement the guidelines previously quoted:

StdCallLibrary.StdCallCallback proc = new StdCallLibrary.StdCallCallback() {
    public WinDef.LRESULT callback(WinDef.HWND hwnd, int uMsg, WinDef.WPARAM wParam, WinDef.LPARAM lParam) {
        if (uMsg == WM_QUERYENDSESSION && lParam.intValue() == ENDSESSION_CLOSEAPP) {
            return new WinDef.LRESULT(WIN_TRUE);
        } else if ((uMsg == WM_ENDSESSION && lParam.intValue() == ENDSESSION_CLOSEAPP && wParam.intValue() == WIN_TRUE) || uMsg == WM_CLOSE) {
            Application.exit();
            return new WinDef.LRESULT(WIN_FALSE); 
        }
        return user32.DefWindowProc(hwnd, uMsg, wParam, lParam);

    }
};

Oh! Lot’s of constants! What are they? I define them in the full example at the bottom of this post. They should be mostly self-evident what they stand for, their actual values are not that important.

Now things get tricky. Apparently Microsoft Windows send these messages to windows, not processes. Dashman can run in the tray bar, with no active window. And even if it had an active window, getting the HWND pointer for that window in JavaFX doesn’t seem trivial (I couldn’t get it to work). So, I create a size 0 invisible window to receive the message:

WinDef.HWND window = user32.CreateWindowEx(0, "STATIC", "Dashman Win32 Restart Manager Window.", WS_MINIMIZE, 0, 0, 0, 0, null, null, null, null);

Then I need to connect that window to the callback:

try {
    user32.SetWindowLongPtr(window, GWL_WNDPROC, proc);
} catch (UnsatisfiedLinkError e) {
    user32.SetWindowLong(window, GWL_WNDPROC, proc);
}

The callback is not magic though, and requires an event loop that will constantly check if there’s a message and trigger the processing when that happens:

WinUser.MSG msg = new WinUser.MSG();
while (user32.GetMessage(msg, null, 0, 0) > 0) {
   user32.TranslateMessage(msg);
   user32.DispatchMessage(msg);
}

Of course, that means you want this to run as its own daemon thread. The reason to make it a daemon thread is so that it won’t hang around preventing the JVM from exiting. 

One of my most useful sources of understanding and inspiration was the source code for Briar. I want to give credit where credit is due. I do think I spotted an issue with their source code in which they are not following the guidelines though. Also, they have a much more complex situation to handle.

And now, the full example with all my comments including links to more information explaining where all the values for constants and logic is coming from:

import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.platform.win32.WinDef;
import com.sun.jna.platform.win32.WinUser;
import com.sun.jna.win32.StdCallLibrary;
import com.sun.jna.win32.W32APIFunctionMapper;
import com.sun.jna.win32.W32APITypeMapper;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import static com.sun.jna.Library.OPTION_FUNCTION_MAPPER;
import static com.sun.jna.Library.OPTION_TYPE_MAPPER;

// Inspiration can be found at https://code.briarproject.org/akwizgran/briar
public class RestartManager {
    // https://autohotkey.com/docs/misc/SendMessageList.htm
    private static final int WM_CLOSE = 0x10; // https://msdn.microsoft.com/en-us/library/windows/desktop/ms632617
    private static final int WM_QUERYENDSESSION = 0x11; // https://msdn.microsoft.com/en-us/library/windows/desktop/aa376890
    private static final int WM_ENDSESSION = 0x16; // https://msdn.microsoft.com/en-us/library/windows/desktop/aa376889

    // https://msdn.microsoft.com/en-us/library/windows/desktop/aa376890
    // https://msdn.microsoft.com/en-us/library/windows/desktop/aa376889
    private static final int ENDSESSION_CLOSEAPP = 0x00000001;
    private static final int ENDSESSION_CRITICAL = 0x40000000;
    private static final int ENDSESSION_LOGOFF = 0x80000000;

    // https://stackoverflow.com/questions/50409858/how-do-i-return-a-boolean-as-a-windef-lresult
    private static final int WIN_FALSE = 0;
    private static final int WIN_TRUE = 1;

    // https://msdn.microsoft.com/en-us/library/windows/desktop/ms633591(v=vs.85).aspx
    private static final int GWL_WNDPROC = -4;

    // https://msdn.microsoft.com/en-us/library/windows/desktop/ms632600(v=vs.85).aspx
    private static final int WS_MINIMIZE = 0x20000000;

    public static void enable() {
        Runnable evenLoopProc = () -> {
            // Load user32.dll usi the Unicode versions of Win32 API calls
            Map options = new HashMap();
            options.put(OPTION_TYPE_MAPPER, W32APITypeMapper.UNICODE);
            options.put(OPTION_FUNCTION_MAPPER, W32APIFunctionMapper.UNICODE);
            User32 user32 = Native.loadLibrary("user32", User32.class, Collections.unmodifiableMap(options));

            // Function that handles the messages according to the Restart Manager Guidelines for Applications.
            // https://msdn.microsoft.com/en-us/library/windows/desktop/aa373651
            StdCallLibrary.StdCallCallback proc = new StdCallLibrary.StdCallCallback() {
                WinDef.LRESULT callback(WinDef.HWND hwnd, int uMsg, WinDef.WPARAM wParam, WinDef.LPARAM lParam) {
                    if (uMsg == WM_QUERYENDSESSION && lParam.intValue() == ENDSESSION_CLOSEAPP) {
                        return new WinDef.LRESULT(WIN_TRUE); // Yes, we can exit whenever you want.
                    } else if ((uMsg == WM_ENDSESSION && lParam.intValue() == ENDSESSION_CLOSEAPP
                            && wParam.intValue() == WIN_TRUE) || uMsg == WM_CLOSE) {
                        Application.exit();
                        return new WinDef.LRESULT(WIN_FALSE); // Done... don't call user32.DefWindowProc.
                    }
                    return user32.DefWindowProc(hwnd, uMsg, wParam, lParam); // Pass the message to the default window procedure

                }
            };

            // Create a native window that will receive the messages.
            WinDef.HWND window = user32.CreateWindowEx(0, "STATIC",
                    "Dashman Win32 Restart Manager Window.", WS_MINIMIZE, 0, 0, 0,
                    0, null, null, null, null);

            // Register the callback
            try {
                user32.SetWindowLongPtr(window, GWL_WNDPROC, proc); // Use SetWindowLongPtr if available (64-bit safe)
            } catch (UnsatisfiedLinkError e) {
                user32.SetWindowLong(window, GWL_WNDPROC, proc); // Use SetWindowLong if SetWindowLongPtr isn't available
            }

            // The actual event loop.
            WinUser.MSG msg = new WinUser.MSG();
            while (user32.GetMessage(msg, null, 0, 0) > 0) {
                user32.TranslateMessage(msg);
                user32.DispatchMessage(msg);
            }
        };

        Thread eventLoopThread = new Thread(evenLoopProc, "Win32 Event Loop");
        eventLoopThread.setDaemon(true); // Make the thread a daemon so it doesn't prevent Dashman from exiting.
        eventLoopThread.start();
    }

    private interface User32 extends StdCallLibrary {
        // https://msdn.microsoft.com/en-us/library/windows/desktop/ms632680(v=vs.85).aspx
        WinDef.HWND CreateWindowEx(int dwExStyle, String lpClassName, String lpWindowName, int dwStyle, int x, int y, int nWidth, int nHeight, WinDef.HWND hWndParent, WinDef.HMENU hMenu, WinDef.HINSTANCE hInstance, Pointer lpParam);

        // https://msdn.microsoft.com/en-us/library/windows/desktop/ms633572(v=vs.85).aspx
        WinDef.LRESULT DefWindowProc(WinDef.HWND hWnd, int Msg, WinDef.WPARAM wParam, WinDef.LPARAM lParam);

        // https://msdn.microsoft.com/en-us/library/windows/desktop/ms633591(v=vs.85).aspx
        WinDef.LRESULT SetWindowLong(WinDef.HWND hWnd, int nIndex, StdCallLibrary.StdCallCallback dwNewLong);

        // https://msdn.microsoft.com/en-us/library/windows/desktop/ms644898(v=vs.85).aspx
        WinDef.LRESULT SetWindowLongPtr(WinDef.HWND hWnd, int nIndex, StdCallLibrary.StdCallCallback dwNewLong);

        // https://msdn.microsoft.com/en-us/library/windows/desktop/ms644936(v=vs.85).aspx
        int GetMessage(WinUser.MSG lpMsg, WinDef.HWND hWnd, int wMsgFilterMin, int wMsgFilterMax);

        // https://msdn.microsoft.com/en-us/library/windows/desktop/ms644955(v=vs.85).aspx
        boolean TranslateMessage(WinUser.MSG lpMsg);

        // https://msdn.microsoft.com/en-us/library/windows/desktop/ms644934(v=vs.85).aspx
        WinDef.LRESULT DispatchMessage(WinUser.MSG lpmsg);
    }
}

And now, my usual question: do you think this should be a reusable open source library? would you use it?
 

My ham radio licensing journey is now over: M0ONP

A friend of my dad introduced me to ham radio when I was 7 years old. When I was 15 or so I passed my beginner’s exam and then I did nothing with it. I got my call sign when I was 24 years old and moving out of Argentina: LU5ARC. I never used it because Argentina is not part of CEPT (and I haven’t gone back except for short holidays).

To get that Argentinean license, I had to take three months of two evenings a week of lessons on theory, Morse code and operating a radio (just making QSOs on 80 meters). I actually collected about 10 QSLs from that time (I wish I knew where they are).

When I moved to the UK almost 7 years ago, I looked into transferring my license but I was told it was impossible. I wish they also told me how easy it was to get a license in the UK and I wouldn’t have waited so long to get started. Last year something else got me interested in radio and I decided to take the plunge and get licensed. I was delighted to see how easy it is.

The hardest part of getting licensed was waiting for the two day course to happen (at that point, I didn’t know about ML&S running them). Because of my previous experience with radio and the fact that I studied electronics and electromechanics in school, there was little to nothing that I didn’t know for the foundation level. Without too much effort I got my first British call sign: M6UON.

Then, I had to wait again and I was thrilled to find that ML&S run foundation and intermediate courses, as well as advanced exams so often. I took the course, pass the exam, and I got my intermediate license: 2E0GGE. A month after that, I took the advanced exam and I now have my full license M0ONP.

Oh… even before there was a foundation course available, I went to the RSGB convention and I took the three exams in a row for the FCC (American) license, so, even before managing to get M6UON, I got an extra (full) one for the US as AC1DM. So ironic!

2018-04-02-15-56-49.jpg

And now the fun begins. I lifted all possible restrictions. I can use the full 100W of my Icom IC-7300 as well as take my Icom ID-51E PLUS2 abroad and use it. I can also supervise unlicensed people so I’ve been introducing all my friends to ham radio. I either have friends that are genuinely interested in this technical hobby that’s going without them knowing about or very good friends that humor me when I spend hours explaining frequency, modulation, SWR, antennas, bandwidth, etc.

Book Review: Advance! The Full Licence Book by Alan Betts, Steve Hartley

51Go0CzjPlL._SX346_BO1,204,203,200_This book felt of much lower quality than the previous two. There are many typos and editing errors and I noticed a few technical errors as well. I guess it makes sense the advanced saw less scrutiny than the beginner one as fewer people will ever get to this point. Or maybe it’s my bias because this is the first time I really need to learn from the book (the previous two were, mostly, revision of stuff I already knew).

What really annoyed me is that there were many explanations that felt it was missing definitions of terms of explanations of the most basic parts so, aside from memorizing the concepts, it was hard to learn from it.

I’m obviously still grateful that there’s a book that covers the syllabus of the test for the Full License. At the same time, I wish we, in the UK, had more and better material like in the US, where you have the Extra Class License Manual, a thick 496-pages long nice book, and the excellent Gordon West’s Extra Class 2012-2016, as well as one or two video courses on YouTube covering all the material.

Giving the the physics is the same for the US and the UK, and that the ham radio hobby is having trouble with not having enough people and resources, it would be nice to share more things across countries. I understand the regulations are different and complex, but still, the actual radio stuff could be exactly the same in the US, UK and many other countries.

★★☆☆☆

Buy Advance! The Full Licence Book

Book Review: Intermediate Licence – Building On The Foundation by Steve Hartley

s-l640I’m not a good person to judge this book because I not only have a technical background in electronics, I also got my American Extra-level license (AC1DM) before this one, so, I had to study all this material a few months ago. This feel like a revision.

Something that really surprises me about the book is that it says the ionosphere reflects radio waves. I understand we use that word in casual speech but I believe a book on ham radio should be more strict and use the correct term: refraction.

For whatever it matters, I passed the exam with a 43 out of 45 questions answered correctly.

★★★☆☆

Buy Intermediate Licence – Building On The Foundation

Restoring window sizes in JavaFX

Update 2018-05-23: Updated the code to my current version, which fixes a few bugs.
When doing usability testing of an alpha version of Dashman, one thing that I was strongly asked was to have the windows remember their sizes when you re-open the application. The need was clear as it was annoying to have the window be a different size when re-started.

The new version of Dashman is built using Java and JavaFX and thus I searched for how to do this, how to restore size. I found many posts, forums, questions, etc all with the same simplistic solution: restoring width and height, and maybe position.

What those were missing was restoring whether the window was maximized (maximized is not the same as occupying all the available space, at least in Windows). But most important than that, none of the solutions took into consideration the fact that the resolutions and quantity of screens could be different than the last time the application run, thus, you could end up with a window completely out of bounds, invisible, immobile.

I came up with this solution, a class that’s designed to be serializable to your config to store the values but also restore them and make sure the window is visible and if not, move it to a visible place:

// Copyright (c) 2017-2018 Flexpoint Tech Ltd. All rights reserved.

package tech.dashman.dashman;

import com.fasterxml.jackson.annotation.JsonIgnore;
import javafx.application.Platform;
import javafx.geometry.Rectangle2D;
import javafx.stage.Screen;
import javafx.stage.Stage;
import lombok.Data;
import tech.dashman.common.Jsonable;

@Data
public class StageSizer implements Jsonable {
    private static double MINIMUM_VISIBLE_WIDTH = 100;
    private static double MINIMUM_VISIBLE_HEIGHT = 50;
    private static double MARGIN = 50;
    private static double DEFAULT_WIDTH = 800;
    private static double DEFAULT_HEIGHT = 600;

    private Boolean maximized = false;
    private Boolean hidden = false;
    private Double x = MARGIN;
    private Double y = MARGIN;
    private Double width = DEFAULT_WIDTH;
    private Double height = DEFAULT_HEIGHT;

    @JsonIgnore
    private Boolean hideable = true;

    @JsonIgnore
    public void setStage(Stage stage) {
        // First, restore the size and position of the stage.
        resizeAndPosition(stage, () -> {
            // If the stage is not visible in any of the current screens, relocate it the primary screen.
            if (isWindowIsOutOfBounds(stage)) {
                moveToPrimaryScreen(stage);
            }
            // And now watch the stage to keep the properties updated.
            watchStage(stage);
        });
    }

    private void resizeAndPosition(Stage stage, Runnable callback) {
        Platform.runLater(() -> {
            if (getHidden() != null && getHidden() && getHideable()) {
                stage.hide();
            }
            if (getX() != null) {
                stage.setX(getX());
            }
            if (getY() != null) {
                stage.setY(getY());
            }
            if (getWidth() != null) {
                stage.setWidth(getWidth());
            } else {
                stage.setWidth(DEFAULT_WIDTH);
            }
            if (getHeight() != null) {
                stage.setHeight(getHeight());
            } else {
                stage.setHeight(DEFAULT_HEIGHT);
            }
            if (getMaximized() != null) {
                stage.setMaximized(getMaximized());
            }
            if (getHidden() == null || !getHidden() || !getHideable()) {
                stage.show();
            }

            new Thread(callback).start();
        });
    }

    public void setHidden(boolean value) {
        this.hidden = value;
    }

    private boolean isWindowIsOutOfBounds(Stage stage) {
        for (Screen screen : Screen.getScreens()) {
            Rectangle2D bounds = screen.getVisualBounds();
            if (stage.getX() + stage.getWidth() - MINIMUM_VISIBLE_WIDTH >= bounds.getMinX() &&
                    stage.getX() + MINIMUM_VISIBLE_WIDTH <= bounds.getMaxX() &&
                    bounds.getMinY() <= stage.getY() && // We want the title bar to always be visible.
                    stage.getY() + MINIMUM_VISIBLE_HEIGHT <= bounds.getMaxY()) {
                return false;
            }
        }
        return true;
    }

    private void moveToPrimaryScreen(Stage stage) {
        Rectangle2D bounds = Screen.getPrimary().getVisualBounds();
        stage.setX(bounds.getMinX() + MARGIN);
        stage.setY(bounds.getMinY() + MARGIN);
        stage.setWidth(DEFAULT_WIDTH);
        stage.setHeight(DEFAULT_HEIGHT);
    }

    private void watchStage(Stage stage) {
        // Get the current values.
        setX(stage.getX());
        setY(stage.getY());
        setWidth(stage.getWidth());
        setHeight(stage.getHeight());
        setMaximized(stage.isMaximized());
        setHidden(!stage.isShowing());
        // Watch for future changes.
        stage.xProperty().addListener((observable, old, x) -> setX((Double) x));
        stage.yProperty().addListener((observable, old, y) -> setY((Double) y));
        stage.widthProperty().addListener((observable, old, width) -> setWidth((Double) width));
        stage.heightProperty().addListener((observable, old, height) -> setHeight((Double) height));
        stage.maximizedProperty().addListener((observable, old, maximized) -> setMaximized(maximized));
        stage.showingProperty().addListener(observable -> setHidden(!stage.isShowing())); // Using an invalidation instead of a change listener due to this weird behaviour: https://stackoverflow.com/questions/50280052/property-not-calling-change-listener-unless-theres-an-invalidation-listener-as
    }
}

and the way you use it is quite simple. On your start method, you create or restore an instance of StageSizer and then do this:

public void start(Stage stage) {
    StageSizer stageSizer = createOrRestoreStageSizerFromConfig();
    stageSizer.setStage(stage);
}

I haven’t put a lot of testing on this code yet but it seems to work. Well, at least on Windows. The problem is that this snippet is interacting with the reality of screen sizes, resolutions, adding and removing monitors, etc. If you find a bug, please, let me know and I might release this a library with the fix so we can keep on collectively improving this.

Book Review: General Class by Gordon West

I bought this book after watching a series of videos on YouTube that mentioned it:

61i+nE392VLThe book has a short introduction and then jumps straight into the question pool for the general class amateur radio exam. For each question, you have the four potential answers, followed by an explanation of the subject and the correct answer. Because the questions and the answer are so close, you might need to use a piece of paper to cover the answer while you think about the question without spoiling it.

I did my study mostly by watching the video and using https://hamstudy.org but the explanations on that website, sometimes, leave a lot to be desire. For quite a few questions, reading the explanations in this book helped a lot. It also has extra snippets of information spread throughout the book that are very nice.

Another positive thing about this book is that it’s full color. It has pictures but most importantly, diagrams and chart making use of the color range to make the information more accessible. Even though I know by heart some of that information, I find myself hopping they would make posters of these charts so I can hang them on my shack: they are beautiful and informative.

Oh… one more thing, I passed the test. Well, I passed the three tests in one sitting.

★★★☆☆

Buy 2015-2019 General Class in USA
Buy 2015-2019 General Class in UK
Buy 2015-2019 General Class in Canada

Book Review: Amateur Radio Exam Secrets by Alan Betts

91lYFNMRVVLDisclaimer: I haven’t read it all, as I’m only going for the foundation level license so I only read the relevant sections and I’ll come back to it when I upgrade to other levels.

I bought this book without knowing anything about it and I’m so glad I did. The book covers all levels of amateur licences in the UK. The way it does it is that each chapter is divided in subsections for each of the levels, so, you can read each chapter up to level you are interested and move on to the next.

Each chapter contains a brief introduction to the subject followed by a set of sample questions like the ones you’d get in the exam. Unlike the American counterpart, the question pool in the UK is not public because you should learn the subject and not memorize answers. Having said that, having some mock tests really helps understand how well prepared you are. There’s even an extra set of questions towards the end.

The answers to all the questions are in an appendix almost at the very end of the book, so, it’s very convenient to avoid accidentally seeing the answer and losing the value of that question. I found thought that going back and forth was annoying and prone to seeing more answers than intended, so, I’d recommend for each section, to do all the questions by writing down the answers on a piece of paper and then checking them against the references.

At the very end of the book you also have the tables, band plan and references that you are allowed during the exam.

I highly recommend this book if you are going to take the exams.

★★★★☆

Buy Amateur Radio Exam Secrets in USA
Buy Amateur Radio Exam Secrets in UK
Buy Amateur Radio Exam Secrets in Canada