Example #1
0
 def validate(self):
     host = self._hostLine.text()
     port = self._portLine.text()
     database = self._databaseLine.text()
     user = self._userLine.text()
     password = self._passwordLine.text()
     e = self.parent().controller().connectDatabase(host, port, database, user, password)
     if not e:
         self.parent().statusBar().showMessage(self.tr("Connected to database <" + database + ">"))
         self.close()
     else:
         err = QErrorMessage(self)
         err.setWindowTitle(self.tr("Unable to connect database"))
         err.showMessage("Connection to database failed \n Error : " + str(e))
         err.show()
Example #2
0
class ExportKarmaDialog(QDialog):
    def __init__(self, parent, modal=True, flags=Qt.WindowFlags()):
        QDialog.__init__(self, parent, flags)
        self.setModal(modal)
        self.setWindowTitle("Export Karma annotations")
        lo = QVBoxLayout(self)
        lo.setMargin(10)
        lo.setSpacing(5)
        # file selector
        self.wfile = FileSelector(self,
                                  label="Filename:",
                                  dialog_label="Karma annotations filename",
                                  default_suffix="ann",
                                  file_types="Karma annotations (*.ann)")
        lo.addWidget(self.wfile)
        # selected sources checkbox
        self.wsel = QCheckBox("selected sources only", self)
        lo.addWidget(self.wsel)
        # OK/cancel buttons
        lo.addSpacing(10)
        lo2 = QHBoxLayout()
        lo.addLayout(lo2)
        lo2.setContentsMargins(0, 0, 0, 0)
        lo2.setMargin(5)
        self.wokbtn = QPushButton("OK", self)
        self.wokbtn.setMinimumWidth(128)
        QObject.connect(self.wokbtn, SIGNAL("clicked()"), self.accept)
        self.wokbtn.setEnabled(False)
        cancelbtn = QPushButton("Cancel", self)
        cancelbtn.setMinimumWidth(128)
        QObject.connect(cancelbtn, SIGNAL("clicked()"), self.reject)
        lo2.addWidget(self.wokbtn)
        lo2.addStretch(1)
        lo2.addWidget(cancelbtn)
        self.setMinimumWidth(384)
        # signals
        QObject.connect(self.wfile, SIGNAL("valid"), self.wokbtn.setEnabled)
        # internal state
        self.qerrmsg = QErrorMessage(self)
        self._model_filename = None

    def setModel(self, model):
        self.model = model
        # set the default annotations filename, whenever a new model filename is set
        filename = self.model.filename()
        if filename and filename != self._model_filename:
            self._model_filename = filename
            self.wfile.setFilename(os.path.splitext(filename)[0] + ".ann")

    def accept(self):
        """Tries to export annotations, and closes the dialog if successful."""
        try:
            filename = self.wfile.filename()
            if os.path.exists(filename) and QMessageBox.question(
                    self, "Exporting Karma annotations",
                    "<P>Overwrite the file %s?</P>" % filename, QMessageBox.Yes
                    | QMessageBox.No, QMessageBox.Yes) != QMessageBox.Yes:
                return
            f = file(self.wfile.filename(), "wt")
            f.write('COORD W\nPA STANDARD\nCOLOR GREEN\nFONT hershey12\n')
            # source list
            if self.wsel.isChecked():
                sources = [src for src in self.model.sources if src.selected]
            else:
                sources = self.model.sources
            # calculate basis size for crosses (TODO: replace min_size with something more sensible, as this value is in degrees)
            brightnesses = [
                abs(src.brightness()) for src in sources
                if src.brightness() != 0
            ]
            min_bright = brightnesses and min(brightnesses)
            min_size = 0.01
            # loop over sources
            busy = BusyIndicator()
            for src in sources:
                ra = src.pos.ra / DEG
                dec = src.pos.dec / DEG
                # figure out source size
                if src.brightness() and min_bright:
                    ysize = (math.log10(abs(src.brightness())) -
                             math.log10(min_bright) + 1) * min_size
                else:
                    ysize = min_size
                xsize = ysize / (math.cos(src.pos.dec) or 1)
                # figure out source style
                style, label = self.model.getSourcePlotStyle(src)
                if style:
                    f.write('# %s\n' % src.name)
                    # write symbol for source
                    f.write('COLOR %s\n' % style.symbol_color)
                    if style.symbol == "plus":
                        f.write('CROSS %.12f %.12f %f %f\n' %
                                (ra, dec, xsize, ysize))
                    elif style.symbol == "cross":
                        f.write('CROSS %.12f %.12f %f %f 45\n' %
                                (ra, dec, ysize, ysize))
                    elif style.symbol == "circle":
                        f.write('CIRCLE %.12f %.12f %f\n' % (ra, dec, ysize))
                    elif style.symbol == "dot":
                        f.write('DOT %.12f %.12f\n' % (ra, dec))
                    elif style.symbol == "square":
                        f.write('CBOX %.12f %.12f %f %f\n' %
                                (ra, dec, xsize, ysize))
                    elif style.symbol == "diamond":
                        f.write('CBOX %.12f %.12f %f %f 45\n' %
                                (ra, dec, xsize, ysize))
                    # write label
                    if label:
                        f.write('FONT hershey%d\n' % (style.label_size * 2))
                        f.write('COLOR %s\n' % style.label_color)
                        f.write('TEXT %.12f %.12f %s\n' % (ra, dec, label))
            f.close()
        except IOError as err:
            busy = None
            self.qerrmsg.showMessage(
                "Error writing Karma annotations file %s: %s" %
                (filename, str(err)))
            return
        busy = None
        self.parent().showMessage(
            "Wrote Karma annotations for %d sources to file %s" %
            (len(sources), filename))
        return QDialog.accept(self)
Example #3
0
class ExportKarmaDialog(QDialog):
    def __init__(self, parent, modal=True, flags=Qt.WindowFlags()):
        QDialog.__init__(self, parent, flags)
        self.setModal(modal)
        self.setWindowTitle("Export Karma annotations")
        lo = QVBoxLayout(self)
        lo.setMargin(10)
        lo.setSpacing(5)
        # file selector
        self.wfile = FileSelector(self, label="Filename:", dialog_label="Karma annotations filename",
                                  default_suffix="ann", file_types="Karma annotations (*.ann)")
        lo.addWidget(self.wfile)
        # selected sources checkbox
        self.wsel = QCheckBox("selected sources only", self)
        lo.addWidget(self.wsel)
        # OK/cancel buttons
        lo.addSpacing(10)
        lo2 = QHBoxLayout()
        lo.addLayout(lo2)
        lo2.setContentsMargins(0, 0, 0, 0)
        lo2.setMargin(5)
        self.wokbtn = QPushButton("OK", self)
        self.wokbtn.setMinimumWidth(128)
        QObject.connect(self.wokbtn, SIGNAL("clicked()"), self.accept)
        self.wokbtn.setEnabled(False)
        cancelbtn = QPushButton("Cancel", self)
        cancelbtn.setMinimumWidth(128)
        QObject.connect(cancelbtn, SIGNAL("clicked()"), self.reject)
        lo2.addWidget(self.wokbtn)
        lo2.addStretch(1)
        lo2.addWidget(cancelbtn)
        self.setMinimumWidth(384)
        # signals
        QObject.connect(self.wfile, SIGNAL("valid"), self.wokbtn.setEnabled)
        # internal state
        self.qerrmsg = QErrorMessage(self)
        self._model_filename = None

    def setModel(self, model):
        self.model = model
        # set the default annotations filename, whenever a new model filename is set
        filename = self.model.filename()
        if filename and filename != self._model_filename:
            self._model_filename = filename
            self.wfile.setFilename(os.path.splitext(filename)[0] + ".ann")

    def accept(self):
        """Tries to export annotations, and closes the dialog if successful."""
        try:
            filename = self.wfile.filename()
            if os.path.exists(filename) and QMessageBox.question(self, "Exporting Karma annotations",
                                                                 "<P>Overwrite the file %s?</P>" % filename,
                                                                 QMessageBox.Yes | QMessageBox.No,
                                                                 QMessageBox.Yes) != QMessageBox.Yes:
                return
            f = file(self.wfile.filename(), "wt")
            f.write('COORD W\nPA STANDARD\nCOLOR GREEN\nFONT hershey12\n')
            # source list
            if self.wsel.isChecked():
                sources = [src for src in self.model.sources if src.selected]
            else:
                sources = self.model.sources
            # calculate basis size for crosses (TODO: replace min_size with something more sensible, as this value is in degrees)
            brightnesses = [abs(src.brightness()) for src in sources if src.brightness() != 0]
            min_bright = brightnesses and min(brightnesses)
            min_size = 0.01
            # loop over sources
            busy = BusyIndicator()
            for src in sources:
                ra = src.pos.ra / DEG
                dec = src.pos.dec / DEG
                # figure out source size
                if src.brightness() and min_bright:
                    ysize = (math.log10(abs(src.brightness())) - math.log10(min_bright) + 1) * min_size
                else:
                    ysize = min_size
                xsize = ysize / (math.cos(src.pos.dec) or 1)
                # figure out source style
                style, label = self.model.getSourcePlotStyle(src)
                if style:
                    f.write('# %s\n' % src.name)
                    # write symbol for source
                    f.write('COLOR %s\n' % style.symbol_color)
                    if style.symbol == "plus":
                        f.write('CROSS %.12f %.12f %f %f\n' % (ra, dec, xsize, ysize))
                    elif style.symbol == "cross":
                        f.write('CROSS %.12f %.12f %f %f 45\n' % (ra, dec, ysize, ysize))
                    elif style.symbol == "circle":
                        f.write('CIRCLE %.12f %.12f %f\n' % (ra, dec, ysize))
                    elif style.symbol == "dot":
                        f.write('DOT %.12f %.12f\n' % (ra, dec))
                    elif style.symbol == "square":
                        f.write('CBOX %.12f %.12f %f %f\n' % (ra, dec, xsize, ysize))
                    elif style.symbol == "diamond":
                        f.write('CBOX %.12f %.12f %f %f 45\n' % (ra, dec, xsize, ysize))
                    # write label
                    if label:
                        f.write('FONT hershey%d\n' % (style.label_size * 2))
                        f.write('COLOR %s\n' % style.label_color)
                        f.write('TEXT %.12f %.12f %s\n' % (ra, dec, label))
            f.close()
        except IOError as err:
            busy = None
            self.qerrmsg.showMessage("Error writing Karma annotations file %s: %s" % (filename, str(err)))
            return
        busy = None
        self.parent().showMessage("Wrote Karma annotations for %d sources to file %s" % (len(sources), filename))
        return QDialog.accept(self)
Example #4
0
class RestoreImageDialog(QDialog):
    def __init__(self, parent, modal=True, flags=Qt.WindowFlags()):
        QDialog.__init__(self, parent, flags)
        self.setModal(modal)
        self.setWindowTitle("Restore model into image")
        lo = QVBoxLayout(self)
        lo.setMargin(10)
        lo.setSpacing(5)
        # file selector
        self.wfile_in = FileSelector(self, label="Input FITS file:", dialog_label="Input FITS file",
                                     default_suffix="fits", file_types="FITS files (*.fits *.FITS)",
                                     file_mode=QFileDialog.ExistingFile)
        lo.addWidget(self.wfile_in)
        self.wfile_out = FileSelector(self, label="Output FITS file:", dialog_label="Output FITS file",
                                      default_suffix="fits", file_types="FITS files (*.fits *.FITS)",
                                      file_mode=QFileDialog.AnyFile)
        lo.addWidget(self.wfile_out)
        # beam size
        lo1 = QHBoxLayout()
        lo.addLayout(lo1)
        lo1.setContentsMargins(0, 0, 0, 0)
        lo1.addWidget(QLabel("Restoring beam FWHM, major axis:", self))
        self.wbmaj = QLineEdit(self)
        lo1.addWidget(self.wbmaj)
        lo1.addWidget(QLabel("\"     minor axis:", self))
        self.wbmin = QLineEdit(self)
        lo1.addWidget(self.wbmin)
        lo1.addWidget(QLabel("\"     P.A.:", self))
        self.wbpa = QLineEdit(self)
        lo1.addWidget(self.wbpa)
        lo1.addWidget(QLabel("\u00B0", self))
        for w in self.wbmaj, self.wbmin, self.wbpa:
            w.setValidator(QDoubleValidator(self))
        lo1 = QHBoxLayout()
        lo.addLayout(lo1)
        lo1.setContentsMargins(0, 0, 0, 0)
        self.wfile_psf = FileSelector(self, label="Set restoring beam by fitting PSF image:",
                                      dialog_label="PSF FITS file", default_suffix="fits",
                                      file_types="FITS files (*.fits *.FITS)", file_mode=QFileDialog.ExistingFile)
        lo1.addSpacing(32)
        lo1.addWidget(self.wfile_psf)
        # selection only
        self.wselonly = QCheckBox("restore selected model sources only", self)
        lo.addWidget(self.wselonly)
        # OK/cancel buttons
        lo.addSpacing(10)
        lo2 = QHBoxLayout()
        lo.addLayout(lo2)
        lo2.setContentsMargins(0, 0, 0, 0)
        lo2.setMargin(5)
        self.wokbtn = QPushButton("OK", self)
        self.wokbtn.setMinimumWidth(128)
        QObject.connect(self.wokbtn, SIGNAL("clicked()"), self.accept)
        self.wokbtn.setEnabled(False)
        cancelbtn = QPushButton("Cancel", self)
        cancelbtn.setMinimumWidth(128)
        QObject.connect(cancelbtn, SIGNAL("clicked()"), self.reject)
        lo2.addWidget(self.wokbtn)
        lo2.addStretch(1)
        lo2.addWidget(cancelbtn)
        self.setMinimumWidth(384)
        # signals
        QObject.connect(self.wfile_in, SIGNAL("filenameSelected"), self._fileSelected)
        QObject.connect(self.wfile_in, SIGNAL("filenameSelected"), self._inputFileSelected)
        QObject.connect(self.wfile_out, SIGNAL("filenameSelected"), self._fileSelected)
        QObject.connect(self.wfile_psf, SIGNAL("filenameSelected"), self._psfFileSelected)
        # internal state
        self.qerrmsg = QErrorMessage(self)

    def setModel(self, model):
        nsel = len([src for src in model.sources if src.selected])
        self.wselonly.setVisible(nsel > 0 and nsel < len(model.sources))
        self.model = model
        self._fileSelected(None)

    def _fileSelected(self, filename):
        self.wokbtn.setEnabled(bool(self.wfile_in.filename() and self.wfile_out.filename()))

    def _inputFileSelected(self, filename):
        if filename:
            try:
                header = pyfits.open(filename)[0].header
            except Exception as err:
                self.qerrmsg.showMessage("Error reading FITS file %s: %s" % (filename, str(err)))
                self.wfile_in.setFilename("")
                return
            # try to get beam extents
            gx, gy, grot = [header.get(x, None) for x in ('BMAJ', 'BMIN', 'BPA')]
            if all([x is not None for x in (gx, gy, grot)]):
                # if beam size is already set, ask before overwriting
                print([str(x.text()) for x in (self.wbmaj, self.wbmin, self.wbpa)])
                if any([bool(str(x.text())) for x in (self.wbmaj, self.wbmin, self.wbpa)]) and \
                        QMessageBox.question(self, "Set restoring beam",
                                             "Also reset restoring beam size from this FITS file?",
                                             QMessageBox.Yes | QMessageBox.No) != QMessageBox.Yes:
                    return
                self.wbmaj.setText("%.2f" % (gx * 3600))
                self.wbmin.setText("%.2f" % (gy * 3600))
                self.wbpa.setText("%.2f" % grot)

    def _psfFileSelected(self, filename):
        busy = BusyIndicator()
        filename = str(filename)
        self.parent().showMessage("Fitting gaussian to PSF file %s" % filename)
        try:
            bmaj, bmin, pa = [x / DEG for x in Imaging.fitPsf(filename)]
        except Exception as err:
            busy = None
            self.qerrmsg.showMessage("Error fitting PSF file %s: %s" % (filename, str(err)))
            return
        bmaj *= 3600 * Imaging.FWHM
        bmin *= 3600 * Imaging.FWHM
        self.wbmaj.setText(str(bmaj))
        self.wbmin.setText(str(bmin))
        self.wbpa.setText(str(pa))

    def accept(self):
        """Tries to restore the image, and closes the dialog if successful."""
        # get list of sources to restore
        sources = self.model.sources
        sel_sources = [src for src in sources if src.selected]
        if len(sel_sources) > 0 and len(sel_sources) < len(sources) and self.wselonly.isChecked():
            sources = sel_sources
        if not sources:
            self.qerrmsg.showMessage("No sources to restore.")
            return
        busy = BusyIndicator()
        # get filenames
        infile = self.wfile_in.filename()
        outfile = self.wfile_out.filename()
        self.parent().showMessage(
            "Restoring %d model sources to image %s, writing to %s" % (len(sources), infile, outfile))
        # read fits file
        try:
            input_hdu = pyfits.open(infile)[0]
        except Exception as err:
            busy = None
            self.qerrmsg.showMessage("Error reading FITS file %s: %s" % (infile, str(err)))
            return
        # get beam sizes
        try:
            bmaj = float(str(self.wbmaj.text()))
            bmin = float(str(self.wbmin.text()))
            pa = float(str(self.wbpa.text()) or "0")
        except Exception as err:
            busy = None
            self.qerrmsg.showMessage("Invalid beam size specified")
            return
        bmaj = bmaj / (Imaging.FWHM * 3600) * DEG
        bmin = bmin / (Imaging.FWHM * 3600) * DEG
        pa = pa * DEG
        # restore
        try:
            Imaging.restoreSources(input_hdu, sources, bmaj, bmin, pa)
        except Exception as err:
            busy = None
            self.qerrmsg.showMessage("Error restoring model into image: %s" % str(err))
            return
        # save fits file
        try:
            input_hdu.writeto(outfile, clobber=True)
        except Exception as err:
            busy = None
            self.qerrmsg.showMessage("Error writing FITS file %s: %s" % (outfile, str(err)))
            return
        self.parent().loadImage(outfile)
        busy = None
        return QDialog.accept(self)
Example #5
0
class MainWindow(QMainWindow):
    ViewModelColumns = ["name", "RA", "Dec", "type", "Iapp", "I", "Q", "U", "V", "RM", "spi", "shape"]

    def __init__(self, parent, hide_on_close=False):
        QMainWindow.__init__(self, parent)
        self.setWindowIcon(pixmaps.tigger_starface.icon())
        self._currier = PersistentCurrier()
        self.hide()
        # init column constants
        for icol, col in enumerate(self.ViewModelColumns):
            setattr(self, "Column%s" % col.capitalize(), icol)
        # init GUI
        self.setWindowTitle("Tigger")
        # self.setIcon(pixmaps.purr_logo.pm())
        cw = QWidget(self)
        self.setCentralWidget(cw)
        cwlo = QVBoxLayout(cw)
        cwlo.setMargin(5)
        # make splitter
        spl1 = self._splitter1 = QSplitter(Qt.Vertical, cw)
        spl1.setOpaqueResize(False)
        cwlo.addWidget(spl1)
        # Create listview of LSM entries
        self.tw = SkyModelTreeWidget(spl1)
        self.tw.hide()

        # split bottom pane
        spl2 = self._splitter2 = QSplitter(Qt.Horizontal, spl1)
        spl2.setOpaqueResize(False)
        self._skyplot_stack = QWidget(spl2)
        self._skyplot_stack_lo = QVBoxLayout(self._skyplot_stack)
        self._skyplot_stack_lo.setContentsMargins(0, 0, 0, 0)

        # add plot
        self.skyplot = SkyModelPlotter(self._skyplot_stack, self)
        self.skyplot.resize(128, 128)
        self.skyplot.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
        self._skyplot_stack_lo.addWidget(self.skyplot, 1000)
        self.skyplot.hide()
        QObject.connect(self.skyplot, SIGNAL("imagesChanged"), self._imagesChanged)
        QObject.connect(self.skyplot, SIGNAL("showMessage"), self.showMessage)
        QObject.connect(self.skyplot, SIGNAL("showErrorMessage"), self.showErrorMessage)

        self._grouptab_stack = QWidget(spl2)
        self._grouptab_stack_lo = lo = QVBoxLayout(self._grouptab_stack)
        self._grouptab_stack_lo.setContentsMargins(0, 0, 0, 0)
        # add groupings table
        self.grouptab = ModelGroupsTable(self._grouptab_stack)
        self.grouptab.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
        QObject.connect(self, SIGNAL("hasSkyModel"), self.grouptab.setEnabled)
        lo.addWidget(self.grouptab, 1000)
        lo.addStretch(1)
        self.grouptab.hide()

        # add image controls -- parentless for now (setLayout will reparent them anyway)
        self.imgman = ImageManager()
        self.skyplot.setImageManager(self.imgman)
        QObject.connect(self.imgman, SIGNAL("imagesChanged"), self._imagesChanged)
        QObject.connect(self.imgman, SIGNAL("showMessage"), self.showMessage)
        QObject.connect(self.imgman, SIGNAL("showErrorMessage"), self.showErrorMessage)

        # enable status line
        self.statusBar().show()
        # Create and populate main menu
        menubar = self.menuBar()
        # File menu
        file_menu = menubar.addMenu("&File")
        qa_open = file_menu.addAction("&Open model...", self._openFileCallback, Qt.CTRL + Qt.Key_O)
        qa_merge = file_menu.addAction("&Merge in model...", self._mergeFileCallback, Qt.CTRL + Qt.SHIFT + Qt.Key_O)
        QObject.connect(self, SIGNAL("hasSkyModel"), qa_merge.setEnabled)
        file_menu.addSeparator()
        qa_save = file_menu.addAction("&Save model", self.saveFile, Qt.CTRL + Qt.Key_S)
        QObject.connect(self, SIGNAL("isUpdated"), qa_save.setEnabled)
        qa_save_as = file_menu.addAction("Save model &as...", self.saveFileAs)
        QObject.connect(self, SIGNAL("hasSkyModel"), qa_save_as.setEnabled)
        qa_save_selection_as = file_menu.addAction("Save selection as...", self.saveSelectionAs)
        QObject.connect(self, SIGNAL("hasSelection"), qa_save_selection_as.setEnabled)
        file_menu.addSeparator()
        qa_close = file_menu.addAction("&Close model", self.closeFile, Qt.CTRL + Qt.Key_W)
        QObject.connect(self, SIGNAL("hasSkyModel"), qa_close.setEnabled)
        qa_quit = file_menu.addAction("Quit", self.close, Qt.CTRL + Qt.Key_Q)

        # Image menu
        menubar.addMenu(self.imgman.getMenu())
        # Plot menu
        menubar.addMenu(self.skyplot.getMenu())

        # LSM Menu
        em = QMenu("&LSM", self)
        self._qa_em = menubar.addMenu(em)
        self._qa_em.setVisible(False)
        QObject.connect(self, SIGNAL("hasSkyModel"), self._qa_em.setVisible)
        self._column_view_menu = QMenu("&Show columns", self)
        self._qa_cv_menu = em.addMenu(self._column_view_menu)
        em.addSeparator()
        em.addAction("Select &all", self._selectAll, Qt.CTRL + Qt.Key_A)
        em.addAction("&Invert selection", self._selectInvert, Qt.CTRL + Qt.Key_I)
        em.addAction("Select b&y attribute...", self._showSourceSelector, Qt.CTRL + Qt.Key_Y)
        em.addSeparator()
        qa_add_tag = em.addAction("&Tag selection...", self.addTagToSelection, Qt.CTRL + Qt.Key_T)
        QObject.connect(self, SIGNAL("hasSelection"), qa_add_tag.setEnabled)
        qa_del_tag = em.addAction("&Untag selection...", self.removeTagsFromSelection, Qt.CTRL + Qt.Key_U)
        QObject.connect(self, SIGNAL("hasSelection"), qa_del_tag.setEnabled)
        qa_del_sel = em.addAction("&Delete selection", self._deleteSelection)
        QObject.connect(self, SIGNAL("hasSelection"), qa_del_sel.setEnabled)

        # Tools menu
        tm = self._tools_menu = QMenu("&Tools", self)
        self._qa_tm = menubar.addMenu(tm)
        self._qa_tm.setVisible(False)
        QObject.connect(self, SIGNAL("hasSkyModel"), self._qa_tm.setVisible)

        # Help menu
        menubar.addSeparator()
        hm = self._help_menu = menubar.addMenu("&Help")
        hm.addAction("&About...", self._showAboutDialog)
        self._about_dialog = None

        # message handlers
        self.qerrmsg = QErrorMessage(self)

        # set initial state
        self.setAcceptDrops(True)
        self.model = None
        self.filename = None
        self._display_filename = None
        self._open_file_dialog = self._merge_file_dialog = self._save_as_dialog = self._save_sel_as_dialog = self._open_image_dialog = None
        self.emit(SIGNAL("isUpdated"), False)
        self.emit(SIGNAL("hasSkyModel"), False)
        self.emit(SIGNAL("hasSelection"), False)
        self._exiting = False

        # set initial layout
        self._current_layout = None
        self.setLayout(self.LayoutEmpty)
        dprint(1, "init complete")

    # layout identifiers
    LayoutEmpty = "empty"
    LayoutImage = "image"
    LayoutImageModel = "model"
    LayoutSplit = "split"

    def _getFilenamesFromDropEvent(self, event):
        """Checks if drop event is valid (i.e. contains a local URL to a FITS file), and returns list of filenames contained therein."""
        dprint(1, "drop event:", event.mimeData().text())
        if not event.mimeData().hasUrls():
            dprint(1, "drop event: no urls")
            return None
        filenames = []
        for url in event.mimeData().urls():
            name = str(url.toLocalFile())
            dprint(2, "drop event: name is", name)
            if name and Images.isFITS(name):
                filenames.append(name)
        dprint(2, "drop event: filenames are", filenames)
        return filenames

    def dragEnterEvent(self, event):
        if self._getFilenamesFromDropEvent(event):
            dprint(1, "drag-enter accepted")
            event.acceptProposedAction()
        else:
            dprint(1, "drag-enter rejected")

    def dropEvent(self, event):
        filenames = self._getFilenamesFromDropEvent(event)
        dprint(1, "dropping", filenames)
        if filenames:
            event.acceptProposedAction()
            busy = BusyIndicator()
            for name in filenames:
                self.imgman.loadImage(name)

    def saveSizes(self):
        if self._current_layout is not None:
            dprint(1, "saving sizes for layout", self._current_layout)
            # save main window size and splitter dimensions
            sz = self.size()
            Config.set('%s-main-window-width' % self._current_layout, sz.width())
            Config.set('%s-main-window-height' % self._current_layout, sz.height())
            for spl, name in ((self._splitter1, "splitter1"), (self._splitter2, "splitter2")):
                ssz = spl.sizes()
                for i, sz in enumerate(ssz):
                    Config.set('%s-%s-size%d' % (self._current_layout, name, i), sz)

    def loadSizes(self):
        if self._current_layout is not None:
            dprint(1, "loading sizes for layout", self._current_layout)
            # get main window size and splitter dimensions
            w = Config.getint('%s-main-window-width' % self._current_layout, 0)
            h = Config.getint('%s-main-window-height' % self._current_layout, 0)
            dprint(2, "window size is", w, h)
            if not (w and h):
                return None
            self.resize(QSize(w, h))
            for spl, name in (self._splitter1, "splitter1"), (self._splitter2, "splitter2"):
                ssz = [Config.getint('%s-%s-size%d' % (self._current_layout, name, i), -1) for i in (0, 1)]
                dprint(2, "splitter", name, "sizes", ssz)
                if all([sz >= 0 for sz in ssz]):
                    spl.setSizes(ssz)
                else:
                    return None
        return True

    def setLayout(self, layout):
        """Changes the current window layout. Restores sizes etc. from config file."""
        if self._current_layout is layout:
            return
        dprint(1, "switching to layout", layout)
        # save sizes to config file
        self.saveSizes()
        # remove imgman widget from all layouts
        for lo in self._skyplot_stack_lo, self._grouptab_stack_lo:
            if lo.indexOf(self.imgman) >= 0:
                lo.removeWidget(self.imgman)
        # assign it to appropriate parent and parent's layout
        if layout is self.LayoutImage or layout is self.LayoutEmpty:
            lo = self._skyplot_stack_lo
        else:
            lo = self._grouptab_stack_lo
        self.imgman.setParent(lo.parentWidget())
        lo.addWidget(self.imgman, 0)
        # show/hide panels
        if layout is self.LayoutEmpty:
            self.tw.hide()
            self.grouptab.hide()
            self.skyplot.show()
        elif layout is self.LayoutImage:
            self.tw.hide()
            self.grouptab.hide()
            self.skyplot.show()
        elif layout is self.LayoutImageModel:
            self.tw.show()
            self.grouptab.show()
            self.skyplot.show()
        # reload sizes
        self._current_layout = layout
        if not self.loadSizes():
            dprint(1, "no sizes loaded, setting defaults")
            if layout is self.LayoutEmpty:
                self.resize(QSize(512, 256))
            elif layout is self.LayoutImage:
                self.resize(QSize(512, 512))
                self._splitter2.setSizes([512, 0])
            elif layout is self.LayoutImageModel:
                self.resize(QSize(1024, 512))
                self._splitter1.setSizes([256, 256])
                self._splitter2.setSizes([256, 256])

    def enableUpdates(self, enable=True):
        """Enables updates of the child widgets. Usually called after startup is completed (i.e. all data loaded)"""
        self.skyplot.enableUpdates(enable)
        if enable:
            if self.model:
                self.setLayout(self.LayoutImageModel)
            elif self.imgman.getImages():
                self.setLayout(self.LayoutImage)
            else:
                self.setLayout(self.LayoutEmpty)
            self.show()

    def _showAboutDialog(self):
        if not self._about_dialog:
            self._about_dialog = AboutDialog.AboutDialog(self)
        self._about_dialog.show()

    def addTool(self, name, callback):
        """Adds a tool to the Tools menu"""
        self._tools_menu.addAction(name, self._currier.curry(self._callTool, callback))

    def _callTool(self, callback):
        callback(self, self.model)

    def _imagesChanged(self):
        """Called when the set of loaded images has changed"""
        if self.imgman.getImages():
            if self._current_layout is self.LayoutEmpty:
                self.setLayout(self.LayoutImage)
        else:
            if not self.model:
                self.setLayout(self.LayoutEmpty)

    def _selectAll(self):
        if not self.model:
            return
        busy = BusyIndicator()
        for src in self.model.sources:
            src.selected = True
        self.model.emitSelection(self)

    def _selectInvert(self):
        if not self.model:
            return
        busy = BusyIndicator()
        for src in self.model.sources:
            src.selected = not src.selected
        self.model.emitSelection(self)

    def _deleteSelection(self):
        unselected = [src for src in self.model.sources if not src.selected]
        nsel = len(self.model.sources) - len(unselected)
        if QMessageBox.question(self, "Delete selection", """<P>Really deleted %d selected source(s)?
        %d unselected sources will remain in the model.</P>""" % (nsel, len(unselected)),
                                QMessageBox.Ok | QMessageBox.Cancel, QMessageBox.Cancel) != QMessageBox.Ok:
            return
        self.model.setSources(unselected)
        self.showMessage("""Deleted %d sources""" % nsel)
        self.model.emitUpdate(SkyModel.UpdateAll, origin=self)

    def _showSourceSelector(self):
        TigGUI.Tools.source_selector.show_source_selector(self, self.model)

    def _updateModelSelection(self, num, origin=None):
        """Called when the model selection has been updated."""
        self.emit(SIGNAL("hasSelection"), bool(num))

    _formats = [f[1] for f in Tigger.Models.Formats.listFormatsFull()]

    _load_file_types = [(doc, ["*" + ext for ext in extensions], load) for load, save, doc, extensions in _formats if
                        load]
    _save_file_types = [(doc, ["*" + ext for ext in extensions], save) for load, save, doc, extensions in _formats if
                        save]

    def showMessage(self, msg, time=3000):
        self.statusBar().showMessage(msg, 3000)

    def showErrorMessage(self, msg, time=3000):
        self.qerrmsg.showMessage(msg)

    def loadImage(self, filename):
        return self.imgman.loadImage(filename)

    def setModel(self, model):
        self.emit(SIGNAL("modelChanged"), model)
        if model:
            self.model = model
            self.emit(SIGNAL("hasSkyModel"), True)
            self.emit(SIGNAL("hasSelection"), False)
            self.emit(SIGNAL("isUpdated"), False)
            self.model.enableSignals()
            self.model.connect("updated", self._indicateModelUpdated)
            self.model.connect("selected", self._updateModelSelection)
            # pass to children
            self.tw.setModel(self.model)
            self.grouptab.setModel(self.model)
            self.skyplot.setModel(self.model)
            # add items to View menu
            self._column_view_menu.clear()
            self.tw.addColumnViewActionsTo(self._column_view_menu)
        else:
            self.model = None
            self.setWindowTitle("Tigger")
            self.emit(SIGNAL("hasSelection"), False)
            self.emit(SIGNAL("isUpdated"), False)
            self.emit(SIGNAL("hasSkyModel"), False)
            self.tw.clear()
            self.grouptab.clear()
            self.skyplot.setModel(None)

    def _openFileCallback(self):
        if not self._open_file_dialog:
            filters = ";;".join(
                ["%s (%s)" % (name, " ".join(patterns)) for name, patterns, func in self._load_file_types])
            dialog = self._open_file_dialog = QFileDialog(self, "Open sky model", ".", filters)
            dialog.setFileMode(QFileDialog.ExistingFile)
            dialog.setModal(True)
            QObject.connect(dialog, SIGNAL("filesSelected(const QStringList &)"), self.openFile)
        self._open_file_dialog.exec_()
        return

    def _mergeFileCallback(self):
        if not self._merge_file_dialog:
            filters = ";;".join(
                ["%s (%s)" % (name, " ".join(patterns)) for name, patterns, func in self._load_file_types])
            dialog = self._merge_file_dialog = QFileDialog(self, "Merge in sky model", ".", filters)
            dialog.setFileMode(QFileDialog.ExistingFile)
            dialog.setModal(True)
            QObject.connect(dialog, SIGNAL("filesSelected(const QStringList &)"),
                            self._currier.curry(self.openFile, merge=True))
        self._merge_file_dialog.exec_()
        return

    def openFile(self, filename=None, format=None, merge=False, show=True):
        # check that we can close existing model
        if not merge and not self._canCloseExistingModel():
            return False
        if isinstance(filename, QStringList):
            filename = filename[0]
        filename = str(filename)
        # try to determine the file type
        filetype, import_func, export_func, doc = Tigger.Models.Formats.resolveFormat(filename, format)
        if import_func is None:
            self.showErrorMessage("""Error loading model file %s: unknown file format""" % filename)
            return
        # try to load the specified file
        busy = BusyIndicator()
        self.showMessage("""Reading %s file %s""" % (filetype, filename), 3000)
        QApplication.flush()
        try:
            model = import_func(filename)
            model.setFilename(filename)
        except:
            busy = None
            self.showErrorMessage("""Error loading '%s' file %s: %s""" % (filetype, filename, str(sys.exc_info()[1])))
            return
        # set the layout
        if show:
            self.setLayout(self.LayoutImageModel)
        # add to content
        if merge and self.model:
            self.model.addSources(model.sources)
            self.showMessage("""Merged in %d sources from '%s' file %s""" % (len(model.sources), filetype, filename),
                             3000)
            self.model.emitUpdate(SkyModel.UpdateAll)
        else:
            self.showMessage("""Loaded %d sources from '%s' file %s""" % (len(model.sources), filetype, filename),
                             3000)
            self._display_filename = os.path.basename(filename)
            self.setModel(model)
            self._indicateModelUpdated(updated=False)
            # only set self.filename if an export function is available for this format. Otherwise set it to None, so that trying to save
            # the file results in a save-as operation (so that we don't save to a file in an unsupported format).
            self.filename = filename if export_func else None

    def closeEvent(self, event):
        dprint(1, "closing")
        self._exiting = True
        self.saveSizes()
        if not self.closeFile():
            self._exiting = False
            event.ignore()
            return
        self.skyplot.close()
        self.imgman.close()
        self.emit(SIGNAL("closing"))
        dprint(1, "invoking os._exit(0)")
        os._exit(0)
        QMainWindow.closeEvent(self, event)

    def _canCloseExistingModel(self):
        # save model if modified
        if self.model and self._model_updated:
            res = QMessageBox.question(self, "Closing sky model",
                                       "<P>Model has been modified, would you like to save the changes?</P>",
                                       QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel, QMessageBox.Save)
            if res == QMessageBox.Cancel:
                return False
            elif res == QMessageBox.Save:
                if not self.saveFile(confirm=False, overwrite=True):
                    return False
        # unload model images, unless we are already exiting anyway
        if not self._exiting:
            self.imgman.unloadModelImages()
        return True

    def closeFile(self):
        if not self._canCloseExistingModel():
            return False
        # close model
        self._display_filename = None
        self.setModel(None)
        # set the layout
        self.setLayout(self.LayoutImage if self.imgman.getTopImage() else self.LayoutEmpty)
        return True

    def saveFile(self, filename=None, confirm=False, overwrite=True, non_native=False):
        """Saves file using the specified 'filename'. If filename is None, uses current filename, if
        that is not set, goes to saveFileAs() to open dialog and get a filename.
        If overwrite=False, will ask for confirmation before overwriting an existing file.
        If non_native=False, will ask for confirmation before exporting in non-native format.
        If confirm=True, will ask for confirmation regardless.
        Returns True if saving succeeded, False on error (or if cancelled by user).
        """
        if isinstance(filename, QStringList):
            filename = filename[0]
        filename = (filename and str(filename)) or self.filename
        if filename is None:
            return self.saveFileAs()
        else:
            warning = ''
            # try to determine the file type
            filetype, import_func, export_func, doc = Tigger.Models.Formats.resolveFormat(filename, None)
            if export_func is None:
                self.showErrorMessage("""Error saving model file %s: unsupported output format""" % filename)
                return
            if os.path.exists(filename) and not overwrite:
                warning += "<P>The file already exists and will be overwritten.</P>"
            if filetype != 'Tigger' and not non_native:
                warning += """<P>Please note that you are exporting the model using the external format '%s'.
              Source types, tags and other model features not supported by this
              format will be omitted during the export.</P>""" % filetype
            # get confirmation
            if confirm or warning:
                dialog = QMessageBox.warning if warning else QMessageBox.question
                if dialog(self, "Saving sky model", "<P>Save model to %s?</P>%s" % (filename, warning),
                          QMessageBox.Save | QMessageBox.Cancel, QMessageBox.Save) != QMessageBox.Save:
                    return False
            busy = BusyIndicator()
            try:
                export_func(self.model, filename)
                self.model.setFilename(filename)
            except:
                busy = None
                self.showErrorMessage("""Error saving model file %s: %s""" % (filename, str(sys.exc_info()[1])))
                return False
            self.showMessage("""Saved model to file %s""" % filename, 3000)
            self._display_filename = os.path.basename(filename)
            self._indicateModelUpdated(updated=False)
            self.filename = filename
            return True

    def saveFileAs(self, filename=None):
        """Saves file using the specified 'filename'. If filename is None, opens dialog to get a filename.
        Returns True if saving succeeded, False on error (or if cancelled by user).
        """
        if filename is None:
            if not self._save_as_dialog:
                filters = ";;".join(
                    ["%s (%s)" % (name, " ".join(patterns)) for name, patterns, func in self._save_file_types])
                dialog = self._save_as_dialog = QFileDialog(self, "Save sky model", ".", filters)
                dialog.setDefaultSuffix(ModelHTML.DefaultExtension)
                dialog.setFileMode(QFileDialog.AnyFile)
                dialog.setAcceptMode(QFileDialog.AcceptSave)
                dialog.setConfirmOverwrite(False)
                dialog.setModal(True)
                QObject.connect(dialog, SIGNAL("filesSelected(const QStringList &)"), self.saveFileAs)
            return self._save_as_dialog.exec_() == QDialog.Accepted
        # filename supplied, so save
        return self.saveFile(filename, confirm=False)

    def saveSelectionAs(self, filename=None, force=False):
        if not self.model:
            return
        if filename is None:
            if not self._save_sel_as_dialog:
                filters = ";;".join(
                    ["%s (%s)" % (name, " ".join(patterns)) for name, patterns, func in self._save_file_types])
                dialog = self._save_sel_as_dialog = QFileDialog(self, "Save sky model", ".", filters)
                dialog.setDefaultSuffix(ModelHTML.DefaultExtension)
                dialog.setFileMode(QFileDialog.AnyFile)
                dialog.setAcceptMode(QFileDialog.AcceptSave)
                dialog.setConfirmOverwrite(True)
                dialog.setModal(True)
                QObject.connect(dialog, SIGNAL("filesSelected(const QStringList &)"), self.saveSelectionAs)
            return self._save_sel_as_dialog.exec_() == QDialog.Accepted
        # save selection
        if isinstance(filename, QStringList):
            filename = filename[0]
        filename = str(filename)
        selmodel = self.model.copy()
        sources = [src for src in self.model.sources if src.selected]
        if not sources:
            self.showErrorMessage("""You have not selected any sources to save.""")
            return
        # try to determine the file type
        filetype, import_func, export_func, doc = Tigger.Models.Formats.resolveFormat(filename, None)
        if export_func is None:
            self.showErrorMessage("""Error saving model file %s: unsupported output format""" % filename)
            return
        busy = BusyIndicator()
        try:
            export_func(self.model, filename, sources=sources)
        except:
            busy = None
            self.showErrorMessage(
                """Error saving selection to model file %s: %s""" % (filename, str(sys.exc_info()[1])))
            return False
        self.showMessage("""Wrote %d selected source%s to file %s""" % (
        len(selmodel.sources), "" if len(selmodel.sources) == 1 else "s", filename), 3000)
        pass

    def addTagToSelection(self):
        if not hasattr(self, '_add_tag_dialog'):
            self._add_tag_dialog = Widgets.AddTagDialog(self, modal=True)
        self._add_tag_dialog.setTags(self.model.tagnames)
        self._add_tag_dialog.setValue(True)
        if self._add_tag_dialog.exec_() != QDialog.Accepted:
            return
        tagname, value = self._add_tag_dialog.getTag()
        if tagname is None or value is None:
            return None
        dprint(1, "tagging selected sources with", tagname, value)
        # tag selected sources
        for src in self.model.sources:
            if src.selected:
                src.setAttribute(tagname, value)
        # If tag is not new, set a UpdateSelectionOnly flag on the signal
        dprint(1, "adding tag to model")
        self.model.addTag(tagname)
        dprint(1, "recomputing totals")
        self.model.getTagGrouping(tagname).computeTotal(self.model.sources)
        dprint(1, "emitting update signal")
        what = SkyModel.UpdateSourceContent + SkyModel.UpdateTags + SkyModel.UpdateSelectionOnly
        self.model.emitUpdate(what, origin=self)

    def removeTagsFromSelection(self):
        if not hasattr(self, '_remove_tag_dialog'):
            self._remove_tag_dialog = Widgets.SelectTagsDialog(self, modal=True, caption="Remove Tags",
                                                               ok_button="Remove")
        # get set of all tags in selected sources
        tags = set()
        for src in self.model.sources:
            if src.selected:
                tags.update(src.getTagNames())
        if not tags:
            return
        tags = list(tags)
        tags.sort()
        # show dialog
        self._remove_tag_dialog.setTags(tags)
        if self._remove_tag_dialog.exec_() != QDialog.Accepted:
            return
        tags = self._remove_tag_dialog.getSelectedTags()
        if not tags:
            return
        # ask for confirmation
        plural = (len(tags) > 1 and "s") or ""
        if QMessageBox.question(self, "Removing tags", "<P>Really remove the tag%s '%s' from selected sources?</P>" % (
        plural, "', '".join(tags)),
                                QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) != QMessageBox.Yes:
            return
        # remove the tags
        for src in self.model.sources:
            if src.selected:
                for tag in tags:
                    src.removeAttribute(tag)
        # update model
        self.model.scanTags()
        self.model.initGroupings()
        # emit signal
        what = SkyModel.UpdateSourceContent + SkyModel.UpdateTags + SkyModel.UpdateSelectionOnly
        self.model.emitUpdate(what, origin=self)

    def _indicateModelUpdated(self, what=None, origin=None, updated=True):
        """Marks model as updated."""
        self._model_updated = updated
        self.emit(SIGNAL("isUpdated"), updated)
        if self.model:
            self.setWindowTitle(
                "Tigger - %s%s" % ((self._display_filename or "(unnamed)", " (modified)" if updated else "")))