Пример #1
0
    def __init__(self):
        ''' Set up user interface from ui_main.py file '''

        sys.excepthook = exception_handler
        QtWidgets.QMainWindow.__init__(self)
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.hide_menu_options()
        self.init_ui()
        self.show()
Пример #2
0
 def __init__(self, app, force_quit=False):
     """ Set up user interface from ui_main.py file. """
     self.app = app
     self.force_quit = force_quit
     sys.excepthook = exception_handler
     QtWidgets.QMainWindow.__init__(self)
     self.ui = Ui_MainWindow()
     self.ui.setupUi(self)
     self.hide_menu_options()
     font = 'font: ' + str(self.app.settings['fontsize']) + 'pt '
     font += '"' + self.app.settings['font'] + '";'
     self.setStyleSheet(font)
     self.init_ui()
     self.show()
Пример #3
0
class MainWindow(QtWidgets.QMainWindow):
    """ Main GUI window.
    Project data is stored in a directory with .qda suffix
    core data is stored in data.qda sqlite file.
    Journal and coding dialogs can be shown non-modally - multiple dialogs open.
    There is a risk of a clash if two coding windows are open with the same file text or
    two journals open with the same journal entry.

    Note: App.settings does not contain projectName, conn or path (to database)
    app.project_name and app.project_path contain these.
    """

    project = {"databaseversion": "", "date": "", "memo": "", "about": ""}
    dialogList = []  # keeps active and track of non-modal windows

    def __init__(self, app, force_quit=False):
        """ Set up user interface from ui_main.py file. """
        self.app = app
        self.force_quit = force_quit
        sys.excepthook = exception_handler
        QtWidgets.QMainWindow.__init__(self)
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.hide_menu_options()
        font = 'font: ' + str(self.app.settings['fontsize']) + 'pt '
        font += '"' + self.app.settings['font'] + '";'
        self.setStyleSheet(font)
        self.init_ui()
        self.show()

    def init_ui(self):
        """ Set up menu triggers """

        # project menu
        self.ui.actionCreate_New_Project.triggered.connect(self.new_project)
        self.ui.actionOpen_Project.triggered.connect(self.open_project)
        self.ui.actionProject_Memo.triggered.connect(self.project_memo)
        self.ui.actionClose_Project.triggered.connect(self.close_project)
        self.ui.actionSettings.triggered.connect(self.change_settings)
        self.ui.actionProject_Exchange_Export.triggered.connect(self.REFI_project_export)
        self.ui.actionREFI_Codebook_export.triggered.connect(self.REFI_codebook_export)
        self.ui.actionREFI_Codebook_import.triggered.connect(self.REFI_codebook_import)
        self.ui.actionREFI_QDA_Project_import.triggered.connect(self.REFI_project_import)
        self.ui.actionRQDA_Project_import.triggered.connect(self.rqda_project_import)
        self.ui.actionExit.triggered.connect(self.closeEvent)

        # file cases and journals menu
        self.ui.actionManage_files.triggered.connect(self.manage_files)
        self.ui.actionManage_journals.triggered.connect(self.journals)
        self.ui.actionManage_cases.triggered.connect(self.manage_cases)
        self.ui.actionManage_attributes.triggered.connect(self.manage_attributes)
        self.ui.actionImport_survey.triggered.connect(self.import_survey)

        # codes menu
        self.ui.actionCodes.triggered.connect(self.text_coding)
        self.ui.actionCode_image.triggered.connect(self.image_coding)
        self.ui.actionCode_audio_video.triggered.connect(self.av_coding)
        self.ui.actionExport_codebook.triggered.connect(self.codebook)

        # reports menu
        self.ui.actionCoding_reports.triggered.connect(self.report_coding)
        self.ui.actionCoding_comparison.triggered.connect(self.report_coding_comparison)
        self.ui.actionCode_frequencies.triggered.connect(self.report_code_frequencies)
        self.ui.actionView_Graph.triggered.connect(self.view_graph_original)
        #TODO self.ui.actionText_mining.triggered.connect(self.text_mining)
        self.ui.actionSQL_statements.triggered.connect(self.report_sql)

        # help menu
        self.ui.actionContents.triggered.connect(self.help)
        self.ui.actionAbout.triggered.connect(self.about)
        font = 'font: ' + str(self.app.settings['fontsize']) + 'pt '
        font += '"' + self.app.settings['font'] + '";'
        self.setStyleSheet(font)
        self.settings_report()

    def hide_menu_options(self):
        """ No project opened, hide most menu options.
         Enable project import options."""

        # project menu
        self.ui.actionClose_Project.setEnabled(False)
        self.ui.actionProject_Memo.setEnabled(False)
        self.ui.actionProject_Exchange_Export.setEnabled(False)
        self.ui.actionREFI_Codebook_export.setEnabled(False)
        self.ui.actionREFI_Codebook_import.setEnabled(False)
        self.ui.actionREFI_QDA_Project_import.setEnabled(True)
        self.ui.actionRQDA_Project_import.setEnabled(True)
        # files cases journals menu
        self.ui.actionManage_files.setEnabled(False)
        self.ui.actionManage_journals.setEnabled(False)
        self.ui.actionManage_cases.setEnabled(False)
        self.ui.actionManage_attributes.setEnabled(False)
        self.ui.actionImport_survey.setEnabled(False)
        # codes menu
        self.ui.actionCodes.setEnabled(False)
        self.ui.actionCode_image.setEnabled(False)
        self.ui.actionCode_audio_video.setEnabled(False)
        self.ui.actionCategories.setEnabled(False)
        self.ui.actionView_Graph.setEnabled(False)
        self.ui.actionExport_codebook.setEnabled(False)
        # reports menu
        self.ui.actionCoding_reports.setEnabled(False)
        self.ui.actionCoding_comparison.setEnabled(False)
        self.ui.actionCode_frequencies.setEnabled(False)
        self.ui.actionText_mining.setEnabled(False)
        self.ui.actionSQL_statements.setEnabled(False)

    def show_menu_options(self):
        """ Project opened, show most menu options.
         Disable project import options. """

        # project menu
        self.ui.actionClose_Project.setEnabled(True)
        self.ui.actionProject_Memo.setEnabled(True)
        self.ui.actionProject_Exchange_Export.setEnabled(True)
        self.ui.actionREFI_Codebook_export.setEnabled(True)
        self.ui.actionREFI_Codebook_import.setEnabled(True)
        self.ui.actionREFI_QDA_Project_import.setEnabled(False)
        self.ui.actionRQDA_Project_import.setEnabled(False)
        # files cases journals menu
        self.ui.actionManage_files.setEnabled(True)
        self.ui.actionManage_journals.setEnabled(True)
        self.ui.actionManage_cases.setEnabled(True)
        self.ui.actionManage_attributes.setEnabled(True)
        self.ui.actionImport_survey.setEnabled(True)
        # codes menu
        self.ui.actionCodes.setEnabled(True)
        self.ui.actionCode_image.setEnabled(True)
        self.ui.actionCode_audio_video.setEnabled(True)
        self.ui.actionCategories.setEnabled(True)
        self.ui.actionView_Graph.setEnabled(True)
        self.ui.actionExport_codebook.setEnabled(True)
        # reports menu
        self.ui.actionCoding_reports.setEnabled(True)
        self.ui.actionCoding_comparison.setEnabled(True)
        self.ui.actionCode_frequencies.setEnabled(True)
        self.ui.actionSQL_statements.setEnabled(True)
        #TODO FOR FUTURE EXPANSION text mining
        self.ui.actionText_mining.setEnabled(False)
        self.ui.actionText_mining.setVisible(False)

    def settings_report(self):
        msg = _("Settings")
        msg += "\n========\n"
        msg += _("Coder") + ": " + self.app.settings['codername'] + "\n"
        msg += _("Font") + ": " + self.app.settings['font'] + " " + str(self.app.settings['fontsize']) + "\n"
        msg += _("Tree font size") + ": " + str(self.app.settings['treefontsize']) + "\n"
        msg += _("Directory") + ": " + self.app.settings['directory'] + "\n"
        msg += _("Project_path:") + self.app.project_path + "\n"
        msg += _("Show IDs") + ": " + str(self.app.settings['showids']) + "\n"
        msg += _("Language") + ": " + self.app.settings['language'] + "\n"
        msg += _("Backup on open") + ": " + str(self.app.settings['backup_on_open']) + "\n"
        msg += _("Backup AV files") + ": " + str(self.app.settings['backup_av_files'])
        msg += "\n========"
        self.ui.textEdit.append(msg)

    def report_sql(self):
        """ Run SQL statements on database. """

        ui = DialogSQL(self.app, self.ui.textEdit)
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    """def text_mining(self):
        ''' text analysis of files / cases / codings.
        NOT CURRENTLY IMPLEMENTED, FOR FUTURE EXPANSION.
        '''

        ui = DialogTextMining(self.app, self.ui.textEdit)
        ui.show()"""

    def report_coding_comparison(self):
        """ Compare two or more coders using Cohens Kappa. """

        for d in self.dialogList:
            if type(d).__name__ == "DialogCoderComparison":
                return
        ui = DialogReportCoderComparisons(self.app, self.ui.textEdit)
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    def report_code_frequencies(self):
        """ Show code frequencies overall and by coder. """

        for d in self.dialogList:
            if type(d).__name__ == "DialogCodeFrequencies":
                return
        ui = DialogReportCodeFrequencies(self.app, self.ui.textEdit)
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    def report_coding(self):
        """ Report on coding and categories. """

        ui = DialogReportCodes(self.app, self.ui.textEdit, self.dialogList)
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    def view_graph_original(self):
        """ Show acyclic graph of codes and categories. """

        ui = ViewGraphOriginal(self.app)
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    def help(self):
        """ Display help information in browser. """

        webbrowser.open(path + "/GUI/QualCoder_Manual.pdf")
        self.clean_dialog_refs()

    def about(self):
        """ About dialog. """

        for d in self.dialogList:
            if type(d).__name__ == "DialogInformation" and d.windowTitle() == "About":
                return
        ui = DialogInformation("About", "")
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    def manage_attributes(self):
        """ Create, edit, delete, rename attributes. """

        ui = DialogManageAttributes(self.app, self.ui.textEdit)
        ui.exec_()
        self.clean_dialog_refs()

    def import_survey(self):
        """ Import survey flat sheet: csv file.
        Create cases and assign attributes to cases.
        Identify qualitative questions and assign these data to the source table for
        coding and review. Modal dialog. """

        ui = DialogImportSurvey(self.app, self.ui.textEdit)
        ui.exec_()
        self.clean_dialog_refs()

    def manage_cases(self):
        """ Create, edit, delete, rename cases, add cases to files or parts of
        files, add memos to cases. """

        for d in self.dialogList:
            if type(d).__name__ == "DialogCases":
                return
        ui = DialogCases(self.app, self.ui.textEdit)
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    def manage_files(self):
        """ Create text files or import files from odt, docx, html and
        plain text. Rename, delete and add memos to files.
        """

        for d in self.dialogList:
            if type(d).__name__ == "DialogManageFiles":
                return
        ui = DialogManageFiles(self.app, self.ui.textEdit)
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    def journals(self):
        """ Create and edit journals. """

        for d in self.dialogList:
            if type(d).__name__ == "DialogJournals":
                return
        ui = DialogJournals(self.app, self.ui.textEdit)
        ui.setAttribute(QtCore.Qt.WA_DeleteOnClose)
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    def text_coding(self):
        """ Create edit and delete codes. Apply and remove codes and annotations to the
        text in imported text files. """

        ui = DialogCodeText(self.app, self.ui.textEdit, self.dialogList)
        ui.setAttribute(QtCore.Qt.WA_DeleteOnClose)
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    def image_coding(self):
        """ Create edit and delete codes. Apply and remove codes to the image (or regions)
        """

        ui = DialogCodeImage(self.app, self.ui.textEdit, self.dialogList)
        ui.setAttribute(QtCore.Qt.WA_DeleteOnClose)
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    def av_coding(self):
        """ Create edit and delete codes. Apply and remove codes to segements of the
        audio or video file. Added try block in case VLC bindings do not work. """

        try:
            ui = DialogCodeAV(self.app, self.ui.textEdit, self.dialogList)
            ui.setAttribute(QtCore.Qt.WA_DeleteOnClose)
            self.dialogList.append(ui)
            ui.show()
        except Exception as e:
            logger.debug(str(e))
            print(e)
            QtWidgets.QMessageBox.warning(None, "A/V Coding", str(e), QtWidgets.QMessageBox.Ok)
        self.clean_dialog_refs()

    def codebook(self):
        """ Export a text file code book of categories and codes.
        """

        Codebook(self.app, self.ui.textEdit)

    def REFI_project_export(self):
        """ Export the project as a qpdx zipped folder.
         Follows the REFI Project Exchange standards.
         CURRENTLY IN TESTING AND NOT COMPLETE NOR VALIDATED.
        VARIABLES ARE NOT SUCCESSFULLY EXPORTED YET.
        CURRENTLY GIFS ARE EXPORTED UNCHANGED (NEED TO BE PNG OR JPG)"""

        Refi_export(self.app, self.ui.textEdit, "project")
        msg = "NOT FULLY TESTED - EXPERIMENTAL\n"
        QtWidgets.QMessageBox.warning(None, "REFI QDA Project export", msg)

    def REFI_codebook_export(self):
        """ Export the codebook as .qdc
        Follows the REFI standard version 1.0. https://www.qdasoftware.org/
        """
        #
        Refi_export(self.app, self.ui.textEdit, "codebook")

    def REFI_codebook_import(self):
        """ Import a codebook .qdc into an opened project.
        Follows the REFI-QDA standard version 1.0. https://www.qdasoftware.org/
         """

        Refi_import(self.app, self.ui.textEdit, "qdc")

    def REFI_project_import(self):
        """ Import a qpdx QDA project into a new project space.
        Follows the REFI standard. """

        self.close_project()
        self.ui.textEdit.append("IMPORTING REFI-QDA PROJECT")
        self.new_project()
        # check project created successfully
        if self.app.project_name == "":
            QtWidgets.QMessageBox.warning(None, "Project creation", "Project not successfully created")
            return

        Refi_import(self.app, self.ui.textEdit, "qdpx")
        msg = "EXPERIMENTAL - NOT FULLY TESTED\n"
        msg += "PDFs not imported\n"
        msg += "Audio, video, transcripts, transcript codings and synchpoints not tested.\n"
        msg += "Sets and Graphs not imported as QualCoders does not have this functionality.\n"
        msg += "External sources over 2GB not imported."
        QtWidgets.QMessageBox.warning(None, "REFI QDA Project import", msg)

    def rqda_project_import(self):
        """ Import an RQDA format project into a new poject space. """

        self.close_project()
        self.ui.textEdit.append("IMPORTING RQDA PROJECT")
        self.new_project()
        # check project created successfully
        if self.app.project_name == "":
            QtWidgets.QMessageBox.warning(None, "Project creation", "Project not successfully created")
            return
        Rqda_import(self.app, self.ui.textEdit)

    def closeEvent(self, event):
        """ Override the QWindow close event.
        Close all dialogs and database connection.
        If selected via menu option exit: event == False
        If selected via window x close: event == QtGui.QCloseEvent
        """

        if not self.force_quit:
            quit_msg = _("Are you sure you want to quit?")
            reply = QtWidgets.QMessageBox.question(self, 'Message', quit_msg,
            QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
            if reply == QtWidgets.QMessageBox.Yes:
                self.dialogList = None
                if self.app.conn is not None:
                    try:
                        self.app.conn.commit()
                        self.app.conn.close()
                    except:
                        pass
                QtWidgets.qApp.quit()
                return
            if event is False:
                return
            else:
                event.ignore()

    def new_project(self):
        """ Create a new project folder with data.qda (sqlite) and folders for documents,
        images, audio and video.
        Note the database does not keep a table specifically for users (coders), instead
        usernames can be freely entered through the settings dialog and are collated from
        coded text, images and a/v.
        v2 had added column in code_text table to link to avid in code_av table.
        """

        self.app = App()
        if self.app.settings['directory'] == "":
            self.app.settings['directory'] = os.path.expanduser('~')
        self.app.project_path = QtWidgets.QFileDialog.getSaveFileName(self,
            _("Enter project name"), self.app.settings['directory'], ".qda")[0]
        if self.app.project_path == "":
            QtWidgets.QMessageBox.warning(None, _("Project"), _("No project created."))
            return
        if self.app.project_path.find(".qda") == -1:
            self.app.project_path = self.app.project_path + ".qda"
        try:
            os.mkdir(self.app.project_path)
            os.mkdir(self.app.project_path + "/images")
            os.mkdir(self.app.project_path + "/audio")
            os.mkdir(self.app.project_path + "/video")
            os.mkdir(self.app.project_path + "/documents")
        except Exception as e:
            logger.critical(_("Project creation error ") + str(e))
            QtWidgets.QMessageBox.warning(None, _("Project"), _("No project created. Exiting. ") + str(e))
            exit(0)
        self.app.project_name = self.app.project_path.rpartition('/')[2]
        self.app.settings['directory'] = self.app.project_path.rpartition('/')[0]
        self.app.create_connection(self.app.project_path)
        cur = self.app.conn.cursor()
        cur.execute("CREATE TABLE project (databaseversion text, date text, memo text,about text);")
        cur.execute("CREATE TABLE source (id integer primary key, name text, fulltext text, mediapath text, memo text, owner text, date text, unique(name));")
        cur.execute("CREATE TABLE code_image (imid integer primary key,id integer,x1 integer, y1 integer, width integer, height integer, cid integer, memo text, date text, owner text);")
        cur.execute("CREATE TABLE code_av (avid integer primary key,id integer,pos0 integer, pos1 integer, cid integer, memo text, date text, owner text);")
        cur.execute("CREATE TABLE annotation (anid integer primary key, fid integer,pos0 integer, pos1 integer, memo text, owner text, date text);")
        cur.execute("CREATE TABLE attribute_type (name text primary key, date text, owner text, memo text, caseOrFile text, valuetype text);")
        cur.execute("CREATE TABLE attribute (attrid integer primary key, name text, attr_type text, value text, id integer, date text, owner text);")
        cur.execute("CREATE TABLE case_text (id integer primary key, caseid integer, fid integer, pos0 integer, pos1 integer, owner text, date text, memo text);")
        cur.execute("CREATE TABLE cases (caseid integer primary key, name text, memo text, owner text,date text, constraint ucm unique(name));")
        cur.execute("CREATE TABLE code_cat (catid integer primary key, name text, owner text, date text, memo text, supercatid integer, unique(name));")
        cur.execute("CREATE TABLE code_text (cid integer, fid integer,seltext text, pos0 integer, pos1 integer, owner text, date text, memo text, avid integer, unique(cid,fid,pos0,pos1, owner));")
        cur.execute("CREATE TABLE code_name (cid integer primary key, name text, memo text, catid integer, owner text,date text, color text, unique(name));")
        cur.execute("CREATE TABLE journal (jid integer primary key, name text, jentry text, date text, owner text);")
        cur.execute("INSERT INTO project VALUES(?,?,?,?)", ('v2',datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),'','QualCoder'))
        self.app.conn.commit()
        try:
            # get and display some project details
            self.ui.textEdit.append("\n" + _("New project: ") + self.app.project_path + _(" created."))
            #self.settings['projectName'] = self.path.rpartition('/')[2]
            self.ui.textEdit.append(_("Opening: ") + self.app.project_path)
            self.setWindowTitle("QualCoder " + self.app.project_name)
            cur.execute('select sqlite_version()')
            self.ui.textEdit.append("SQLite version: " + str(cur.fetchone()))
            cur.execute("select databaseversion, date, memo, about from project")
            result = cur.fetchone()
            self.project['databaseversion'] = result[0]
            self.project['date'] = result[1]
            self.project['memo'] = result[2]
            self.project['about'] = result[3]
            self.ui.textEdit.append(_("New Project Created") + "\n========\n"
                + _("DB Version:") + str(self.project['databaseversion']) + "\n"
                + _("Date: ") + str(self.project['date']) + "\n"
                + _("About: ") + str(self.project['about']) + "\n"
                + _("Coder:") + str(self.app.settings['codername']) + "\n"
                + "========")
        except Exception as e:
            msg = _("Problem creating database ")
            logger.warning(msg + self.app.project_path + " Exception:" + str(e))
            self.ui.textEdit.append("\n" + msg + "\n" + self.app.project_path)
            self.ui.textEdit.append(str(e))
            self.close_project()
            return
        self.open_project(self.app.project_path)

    def change_settings(self):
        """ Change default settings - the coder name, font, font size. Non-modal.
        Backup options """

        ui = DialogSettings(self.app)
        ui.exec_()
        self.settings_report()
        font = 'font: ' + str(self.app.settings['fontsize']) + 'pt '
        font += '"' + self.app.settings['font'] + '";'
        self.setStyleSheet(font)

    def project_memo(self):
        """ Give the entire project a memo. Modal dialog. """

        cur = self.app.conn.cursor()
        cur.execute("select memo from project")
        memo = cur.fetchone()[0]
        ui = DialogMemo(self.app, _("Memo for project ") + self.app.project_name,
            memo)
        self.dialogList.append(ui)
        ui.exec_()
        if memo != ui.memo:
            cur.execute('update project set memo=?', (ui.memo,))
            self.app.conn.commit()
            self.ui.textEdit.append(_("Project memo entered."))

    def open_project(self, path=""):
        """ Open an existing project.
        Also save a backup datetime stamped copy at the same time. """

        if self.app.project_name != "":
            self.close_project()
        self.setWindowTitle("QualCoder" + _("Open Project"))
        if path == "" or path is False:
            #print("appsettings dir ", self.app.settings['directory'])  # tmp
            path = QtWidgets.QFileDialog.getExistingDirectory(self,
                _('Open project directory'), self.app.settings['directory'])
        if path == "" or path is False:
            return
        msg = ""
        if len(path) > 3 and path[-4:] == ".qda":
            try:
                self.app.create_connection(path)
            except Exception as e:
                self.app.conn = None
                msg += " " + str(e)
                logger.debug(msg)
        if self.app.conn is None:
            msg += "\n" + path
            QtWidgets.QMessageBox.warning(None, _("Cannot open file"),
                msg)
            self.app.project_path = ""
            self.app.project_name = ""
            return

        # get and display some project details
        self.app.append_recent_project(self.app.project_path)
        self.setWindowTitle("QualCoder " + self.app.project_name)
        cur = self.app.conn.cursor()
        cur.execute("select databaseversion, date, memo, about from project")
        result = cur.fetchall()[-1]
        self.project['databaseversion'] = result[0]
        self.project['date'] = result[1]
        self.project['memo'] = result[2]
        self.project['about'] = result[3]

        # check avid column in code_text table
        # database version < 2
        try:
            cur.execute("select avid from code_text")
        except:
            cur.execute("ALTER TABLE code_text ADD avid integer;")
            self.app.conn.commit()

        # Save a datetime stamped backup
        if self.app.settings['backup_on_open'] == 'True':
            nowdate = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
            backup = self.app.project_path[0:-4] + "_BACKUP_" + nowdate + ".qda"
            if self.app.settings['backup_av_files'] == 'True':
                shutil.copytree(self.app.project_path, backup)
            else:
                shutil.copytree(self.app.project_path, backup, ignore=shutil.ignore_patterns('*.mp3','*.wav','*.mp4', '*.mov','*.ogg','*.wmv','*.MP3','*.WAV','*.MP4', '*.MOV','*.OGG','*.WMV'))
                self.ui.textEdit.append(_("WARNING: audio and video files NOT backed up. See settings."))
            self.ui.textEdit.append(_("Project backup created: ") + backup)

        self.ui.textEdit.append(_("Project Opened: ") + self.app.project_name
            + "\n========\n"
            + _("Path: ") + self.app.project_path + "\n"
            + _("Directory: ") + self.app.settings['directory'] + "\n"
            + _("Database version: ") + self.project['databaseversion'] + ". "
            + _("Date: ") + str(self.project['date']) + "\n"
            + _("About: ") + self.project['about']
            + "\n========\n")
        self.show_menu_options()

    def close_project(self):
        """ Close an open project. """

        self.ui.textEdit.append("Closing project: " + self.app.project_name + "\n========\n")
        try:
            self.app.conn.commit()
            self.app.conn.close()
        except:
            pass
        self.app.conn = None
        self.app.project_path = ""
        self.app.project_name = ""
        self.app.settings['directory'] = ""
        self.project = {"databaseversion": "", "date": "", "memo": "", "about": ""}
        self.hide_menu_options()
        self.clean_dialog_refs()

    def clean_dialog_refs(self):
        """ Test the list of dialog refs to see if they have been cleared
        and create a new list of current dialogs.
        Also need to keep these dialog references to keep non-modal dialogs open.
        Non-modal example - having a journal open and a coding dialog. """

        tempList = []
        for d in self.dialogList:
            try:
                #logger.debug(str(d) + ", isVisible:" + str(d.isVisible()) + " Title:" + d.windowTitle())
                #d.windowTitle()
                if d.isVisible():
                    tempList.append(d)
            # RuntimeError: wrapped C/C++ object of type DialogSQL has been deleted
            except RuntimeError as e:
                logger.error(str(e))
        self.dialogList = tempList
        self.update_dialog_lists_in_modeless_dialogs()

    def update_dialog_lists_in_modeless_dialogs(self):
        ''' This is to assist: Update code and category tree in DialogCodeImage,
        DialogCodeAV, DialogCodeText, DialogReportCodes '''

        for d in self.dialogList:
            if isinstance(d, DialogCodeText):
                d.dialog_list = self.dialogList
            if isinstance(d, DialogCodeAV):
                d.dialog_list = self.dialogList
            if isinstance(d, DialogCodeImage):
                d.dialog_list = self.dialogList
            if isinstance(d, DialogReportCodes):
                d.dialog_list = self.dialogList
Пример #4
0
class MainWindow(QtWidgets.QMainWindow):
    """ Main GUI window.
    Project data is stored in a directory with .qda suffix
    core data is stored in data.qda sqlite file.
    Journal and coding dialogs can be shown non-modally - multiple dialogs open.
    There is a risk of a clash if two coding windows are open with the same file text or
    two journals open with the same journal entry. """

    settings = {
        "conn": None,
        "directory": home,
        "projectName": "",
        "showIDs": False,
        'path': home,
        "codername": "default",
        "font": "Noto Sans",
        "fontsize": 10,
        'treefontsize': 10,
        "language": "en"
    }
    project = {"databaseversion": "", "date": "", "memo": "", "about": ""}
    dialogList = []  # keeps active and track of non-modal windows

    def __init__(self):
        """ Set up user interface from ui_main.py file. """

        sys.excepthook = exception_handler
        QtWidgets.QMainWindow.__init__(self)
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.hide_menu_options()
        self.init_ui()
        self.conn = None
        self.show()

    def init_ui(self):
        """ Set up menu triggers """

        # project menu
        self.ui.actionCreate_New_Project.triggered.connect(self.new_project)
        self.ui.actionOpen_Project.triggered.connect(self.open_project)
        self.ui.actionProject_Memo.triggered.connect(self.project_memo)
        self.ui.actionClose_Project.triggered.connect(self.close_project)
        self.ui.actionSettings.triggered.connect(self.change_settings)
        self.ui.actionProject_Exchange_Export.triggered.connect(
            self.project_exchange_export)
        self.ui.actionExit.triggered.connect(self.closeEvent)

        # file cases and journals menu
        self.ui.actionManage_files.triggered.connect(self.manage_files)
        self.ui.actionManage_journals.triggered.connect(self.journals)
        self.ui.actionManage_cases.triggered.connect(self.manage_cases)
        self.ui.actionManage_attributes.triggered.connect(
            self.manage_attributes)
        self.ui.actionImport_survey.triggered.connect(self.import_survey)

        # codes menu
        self.ui.actionCodes.triggered.connect(self.text_coding)
        self.ui.actionCode_image.triggered.connect(self.image_coding)
        self.ui.actionCode_audio_video.triggered.connect(self.av_coding)
        self.ui.actionExport_codebook.triggered.connect(self.codebook)
        self.ui.actionView_Graph.triggered.connect(self.view_graph)

        # reports menu
        self.ui.actionCoding_reports.triggered.connect(self.report_coding)
        self.ui.actionCoding_comparison.triggered.connect(
            self.report_coding_comparison)
        self.ui.actionCode_frequencies.triggered.connect(
            self.report_code_frequencies)
        #TODO self.ui.actionText_mining.triggered.connect(self.text_mining)
        self.ui.actionSQL_statements.triggered.connect(self.report_sql)

        # help menu
        self.ui.actionContents.triggered.connect(self.help)
        self.ui.actionAbout.triggered.connect(self.about)

        # load_settings from file stored in home/.qualcoder/
        try:
            with open(home + '/.qualcoder/QualCoder_settings.txt') as f:
                txt = f.read()
                txt = txt.split("\n")
                self.settings['codername'] = txt[0]
                self.settings['font'] = txt[1]
                self.settings['fontsize'] = int(txt[2])
                self.settings['treefontsize'] = int(txt[3])
                self.settings['directory'] = txt[4]
                self.settings['showIDs'] = False
                if txt[5] == "True":
                    self.settings['showIDs'] = True
                # With workarounds for an empty or non-existant language line
                try:
                    self.settings['language'] = txt[6]
                    if txt[6] == "":
                        self.settings['language'] = "en"
                except:
                    self.settings['language'] = "en"
        except:
            f = open(home + '/.qualcoder/QualCoder_settings.txt', 'w')
            f.write("default\nNoto Sans\n10\n10\n" + home + "\nFalse\nen")
            f.close()
        new_font = QtGui.QFont(self.settings['font'],
                               self.settings['fontsize'], QtGui.QFont.Normal)
        self.setFont(new_font)
        self.settings_report()

    def hide_menu_options(self):
        """ No project opened, hide these menu options """

        self.ui.actionClose_Project.setEnabled(False)
        self.ui.actionProject_Memo.setEnabled(False)
        self.ui.actionProject_Exchange_Export.setEnabled(False)
        # files cases journals menu
        self.ui.actionManage_files.setEnabled(False)
        self.ui.actionManage_journals.setEnabled(False)
        self.ui.actionManage_cases.setEnabled(False)
        self.ui.actionManage_attributes.setEnabled(False)
        self.ui.actionImport_survey.setEnabled(False)
        # codes menu
        self.ui.actionCodes.setEnabled(False)
        self.ui.actionCode_image.setEnabled(False)
        self.ui.actionCode_audio_video.setEnabled(False)
        self.ui.actionCategories.setEnabled(False)
        self.ui.actionView_Graph.setEnabled(False)
        self.ui.actionExport_codebook.setEnabled(False)
        # reports menu
        self.ui.actionCoding_reports.setEnabled(False)
        self.ui.actionCoding_comparison.setEnabled(False)
        self.ui.actionCode_frequencies.setEnabled(False)
        self.ui.actionText_mining.setEnabled(False)
        self.ui.actionSQL_statements.setEnabled(False)

    def show_menu_options(self):
        """ Project opened, show these menu options """

        self.ui.actionClose_Project.setEnabled(True)
        self.ui.actionProject_Memo.setEnabled(True)
        self.ui.actionProject_Exchange_Export.setEnabled(True)
        # files cases journals menu
        self.ui.actionManage_files.setEnabled(True)
        self.ui.actionManage_journals.setEnabled(True)
        self.ui.actionManage_cases.setEnabled(True)
        self.ui.actionManage_attributes.setEnabled(True)
        self.ui.actionImport_survey.setEnabled(True)
        # codes menu
        self.ui.actionCodes.setEnabled(True)
        self.ui.actionCode_image.setEnabled(True)
        self.ui.actionCode_audio_video.setEnabled(True)
        self.ui.actionCategories.setEnabled(True)
        self.ui.actionView_Graph.setEnabled(True)
        self.ui.actionExport_codebook.setEnabled(True)
        # reports menu
        self.ui.actionCoding_reports.setEnabled(True)
        self.ui.actionCoding_comparison.setEnabled(True)
        self.ui.actionCode_frequencies.setEnabled(True)
        self.ui.actionSQL_statements.setEnabled(True)
        #TODO FOR FUTURE EXPANSION text mining
        self.ui.actionText_mining.setEnabled(False)

    def settings_report(self):
        msg = _("Settings")
        msg += "\n========\n"
        msg += _("Coder") + ": " + self.settings['codername'] + "\n"
        msg += _("Font") + ": " + self.settings['font'] + " " + str(
            self.settings['fontsize'])
        msg += ". "
        msg += _("Code font size") + ": " + self.settings['font'] + " " + str(
            self.settings['treefontsize']) + "\n"
        msg += _("Directory") + ": " + self.settings['directory'] + "\n"
        msg += _("Show IDs") + ": " + str(self.settings['showIDs']) + "\n"
        msg += _("Language") + ": " + self.settings['language']
        msg += "\n========"
        self.ui.textEdit.append(msg)

    def report_sql(self):
        """ Run SQL statements on database. """

        ui = DialogSQL(self.settings, self.ui.textEdit)
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    """def text_mining(self):
        ''' text analysis of files / cases / codings.
        NOT CURRENTLY IMPLEMENTED, FOR FUTURE EXPANSION.
        '''

        ui = DialogTextMining(self.settings, self.ui.textEdit)
        ui.show()"""

    def report_coding_comparison(self):
        """ Compare two or more coders using Cohens Kappa. """

        for d in self.dialogList:
            if type(d).__name__ == "DialogCoderComparison":
                return
        ui = DialogReportCoderComparisons(self.settings, self.ui.textEdit)
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    def report_code_frequencies(self):
        """ Show code frequencies overall and by coder. """

        for d in self.dialogList:
            if type(d).__name__ == "DialogCodeFrequencies":
                return
        ui = DialogReportCodeFrequencies(self.settings, self.ui.textEdit)
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    def report_coding(self):
        """ Report on coding and categories. """

        ui = DialogReportCodes(self.settings, self.ui.textEdit)
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    def view_graph(self):
        """ Show acyclic graph of codes and categories. """

        ui = ViewGraph(self.settings)
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    def help(self):
        """ Help dialog. """

        ui = DialogInformation("Help contents", "GUI/en_Help.html")
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    def about(self):
        """ About dialog. """

        for d in self.dialogList:
            if type(d).__name__ == "DialogInformation" and d.windowTitle(
            ) == "About":
                return
        ui = DialogInformation("About", "GUI/About.html")
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    def manage_attributes(self):
        """ Create, edit, delete, rename attributes. """

        ui = DialogManageAttributes(self.settings, self.ui.textEdit)
        ui.exec_()
        self.clean_dialog_refs()

    def import_survey(self):
        """ Import survey flat sheet: csv file.
        Create cases and assign attributes to cases.
        Identify qualitative questions and assign these data to the source table for
        coding and review. Modal dialog. """

        ui = DialogImportSurvey(self.settings, self.ui.textEdit)
        ui.exec_()
        self.clean_dialog_refs()

    def manage_cases(self):
        """ Create, edit, delete, rename cases, add cases to files or parts of
        files, add memos to cases. """

        for d in self.dialogList:
            if type(d).__name__ == "DialogCases":
                return
        ui = DialogCases(self.settings, self.ui.textEdit)
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    def manage_files(self):
        """ Create text files or import files from odt, docx, html and
        plain text. Rename, delete and add memos to files.
        """

        for d in self.dialogList:
            if type(d).__name__ == "DialogManageFiles":
                return
        ui = DialogManageFiles(self.settings, self.ui.textEdit)
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    def journals(self):
        """ Create and edit journals. """

        for d in self.dialogList:
            if type(d).__name__ == "DialogJournals":
                return
        ui = DialogJournals(self.settings, self.ui.textEdit)
        ui.setAttribute(QtCore.Qt.WA_DeleteOnClose)
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    def text_coding(self):
        """ Create edit and delete codes. Apply and remove codes and annotations to the
        text in imported text files. """

        ui = DialogCodeText(self.settings, self.ui.textEdit)
        ui.setAttribute(QtCore.Qt.WA_DeleteOnClose)
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    def image_coding(self):
        """ Create edit and delete codes. Apply and remove codes to the image (or regions)
        """

        ui = DialogCodeImage(self.settings, self.ui.textEdit)
        ui.setAttribute(QtCore.Qt.WA_DeleteOnClose)
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    def av_coding(self):
        """ Create edit and delete codes. Apply and remove codes to segements of the
        audio or video file. Added try block in case VLC bindings do not work. """

        try:
            ui = DialogCodeAV(self.settings, self.ui.textEdit)
            ui.setAttribute(QtCore.Qt.WA_DeleteOnClose)
            self.dialogList.append(ui)
            ui.show()
        except Exception as e:
            logger.debug(str(e))
            print(e)
            QtWidgets.QMessageBox.warning(None, "A/V Coding", str(e),
                                          QtWidgets.QMessageBox.Ok)
        self.clean_dialog_refs()

    def codebook(self):
        """ Export a text file code book of categories and codes.
        """

        Codebook(self.settings, self.ui.textEdit)

    def project_exchange_export(self):
        """ Export the project as a qpdx zipped folder.
         Follows the REFI Project Exchange standards.
         CURRENTLY IN TESTING AND NOT COMPLETE NOR VALIDATED.
        VARIABLES ARE NOT SUCCESSFULLY EXPORTED YET.
        CURRENTLY GIFS ARE EXPORTED UNCHANGED (NEED TO BE PNG OR JPG)"""

        Refi(self.settings, self.ui.textEdit)

    def closeEvent(self, event):
        """ Override the QWindow close event.
        Close all dialogs and database connection.
        If selected via menu option exit: event == False
        If selected via window x close: event == QtGui.QCloseEvent
        """

        quit_msg = _("Are you sure you want to quit?")
        reply = QtWidgets.QMessageBox.question(self, 'Message', quit_msg,
                                               QtWidgets.QMessageBox.Yes,
                                               QtWidgets.QMessageBox.No)
        if reply == QtWidgets.QMessageBox.Yes:
            self.dialogList = None
            if self.settings['conn'] is not None:
                try:
                    self.settings['conn'].commit()
                    self.settings['conn'].close()
                except:
                    pass
            QtWidgets.qApp.quit()
            return
        if event is False:
            return
        else:
            event.ignore()

    def new_project(self):
        """ Create a new project folder with data.qda (sqlite) and folders for documents,
        images, audio and video. """

        #logger.debug("settings[directory]:" + self.settings['directory'])
        self.settings['path'] = QtWidgets.QFileDialog.getSaveFileName(
            self, _("Enter project name"), self.settings['directory'],
            ".qda")[0]
        if self.settings['path'] == "":
            QtWidgets.QMessageBox.warning(None, _("Project"),
                                          _("No project created."))
            return
        if self.settings['path'].find(".qda") == -1:
            self.settings['path'] = self.settings['path'] + ".qda"
        try:
            os.mkdir(self.settings['path'])
            os.mkdir(self.settings['path'] + "/images")
            os.mkdir(self.settings['path'] + "/audio")
            os.mkdir(self.settings['path'] + "/video")
            os.mkdir(self.settings['path'] + "/documents")
        except Exception as e:
            logger.critical(_("Project creation error ") + str(e))
            QtWidgets.QMessageBox.warning(
                None, _("Project"),
                _("No project created. Exiting. ") + str(e))
            exit(0)
        self.settings['projectName'] = self.settings['path'].rpartition('/')[2]
        self.settings['directory'] = self.settings['path'].rpartition('/')[0]
        #try:
        self.settings['conn'] = sqlite3.connect(self.settings['path'] +
                                                "/data.qda")
        cur = self.settings['conn'].cursor()
        cur.execute(
            "CREATE TABLE project (databaseversion text, date text, memo text,about text);"
        )
        cur.execute(
            "CREATE TABLE source (id integer primary key, name text, fulltext text, mediapath text, memo text, owner text, date text, unique(name));"
        )
        cur.execute(
            "CREATE TABLE code_image (imid integer primary key,id integer,x1 integer, y1 integer, width integer, height integer, cid integer, memo text, date text, owner text);"
        )
        cur.execute(
            "CREATE TABLE code_av (avid integer primary key,id integer,pos0 integer, pos1 integer, cid integer, memo text, date text, owner text);"
        )
        cur.execute(
            "CREATE TABLE annotation (anid integer primary key, fid integer,pos0 integer, pos1 integer, memo text, owner text, date text);"
        )
        cur.execute(
            "CREATE TABLE attribute_type (name text primary key, date text, owner text, memo text, caseOrFile text, valuetype text);"
        )
        cur.execute(
            "CREATE TABLE attribute (attrid integer primary key, name text, attr_type text, value text, id integer, date text, owner text);"
        )
        cur.execute(
            "CREATE TABLE case_text (id integer primary key, caseid integer, fid integer, pos0 integer, pos1 integer, owner text, date text, memo text);"
        )
        cur.execute(
            "CREATE TABLE cases (caseid integer primary key, name text, memo text, owner text,date text, constraint ucm unique(name));"
        )
        cur.execute(
            "CREATE TABLE code_cat (catid integer primary key, name text, owner text, date text, memo text, supercatid integer, unique(name));"
        )
        cur.execute(
            "CREATE TABLE code_text (cid integer, fid integer,seltext text, pos0 integer, pos1 integer, owner text, date text, memo text, unique(cid,fid,pos0,pos1, owner));"
        )
        cur.execute(
            "CREATE TABLE code_name (cid integer primary key, name text, memo text, catid integer, owner text,date text, color text, unique(name));"
        )
        cur.execute(
            "CREATE TABLE journal (jid integer primary key, name text, jentry text, date text, owner text);"
        )
        cur.execute(
            "INSERT INTO project VALUES(?,?,?,?)",
            ('v1', datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), '',
             'QualCoder'))
        self.settings['conn'].commit()
        try:
            # get and display some project details
            self.ui.textEdit.append("\n" + _("New project: ") +
                                    self.settings['path'] + _(" created."))
            #self.settings['projectName'] = self.path.rpartition('/')[2]
            self.ui.textEdit.append(_("Opening: ") + self.settings['path'])
            self.setWindowTitle("QualCoder " + self.settings['projectName'])
            cur = self.settings['conn'].cursor()
            cur.execute('select sqlite_version()')
            self.ui.textEdit.append("SQLite version: " + str(cur.fetchone()))
            cur.execute(
                "select databaseversion, date, memo, about from project")
            result = cur.fetchone()
            self.project['databaseversion'] = result[0]
            self.project['date'] = result[1]
            self.project['memo'] = result[2]
            self.project['about'] = result[3]
            self.ui.textEdit.append(
                _("New Project Created") + "\n========\n" + _("DB Version:") +
                str(self.project['databaseversion']) + "\n" + _("Date: ") +
                str(self.project['date']) + "\n" + _("About: ") +
                str(self.project['about']) + "\n" + _("Coder:") +
                str(self.settings['codername']) + "\n" + "========")
        except Exception as e:
            msg = _("Problem creating database ")
            logger.warning(msg + self.settings['path'] + " Exception:" +
                           str(e))
            self.ui.textEdit.append("\n" + msg + "\n" + self.settings['path'])
            self.ui.textEdit.append(str(e))
            self.close_project()
            return
        self.open_project(self.settings['path'])

    def change_settings(self):
        """ Change default settings - the coder name, font, font size. Non-modal. """

        ui = DialogSettings(self.settings)
        ui.exec_()
        self.settings_report()
        newfont = QtGui.QFont(self.settings['font'], self.settings['fontsize'],
                              QtGui.QFont.Normal)
        self.setFont(newfont)

    def project_memo(self):
        """ Give the entire project a memo. Modal dialog. """

        cur = self.settings['conn'].cursor()
        cur.execute("select memo from project")
        memo = cur.fetchone()[0]
        ui = DialogMemo(self.settings,
                        _("Memo for project ") + self.settings['projectName'],
                        memo)
        self.dialogList.append(ui)
        ui.exec_()
        if memo != ui.memo:
            cur.execute('update project set memo=?', (ui.memo, ))
            self.settings['conn'].commit()
            self.ui.textEdit.append(_("Project memo entered."))

    def open_project(self, path=""):
        """ Open an existing project.
        Also save a backup datetime stamped copy at the same time. """

        if self.settings['projectName'] != "":
            self.close_project()
        self.setWindowTitle("QualCoder" + _("Open Project"))
        if path == "" or path is False:
            path = QtWidgets.QFileDialog.getExistingDirectory(
                self, _('Open project directory'), self.settings['directory'])
        if path == "" or path is False:
            return
        if len(path) > 3 and path[-4:] == ".qda":
            self.settings['path'] = path
            msg = ""
            try:
                self.settings['conn'] = sqlite3.connect(self.settings['path'] +
                                                        "/data.qda")
            except Exception as e:
                self.settings['conn'] = None
                msg += str(e)
                logger.debug(str(e))
        if self.settings['conn'] is None:
            QtWidgets.QMessageBox.warning(
                None, _("Cannot open file"),
                self.settings['path'] + _(" is not a .qda file "))
            self.settings['path'] = ""
            return
        # get and display some project details
        self.settings['path'] = path
        self.settings['projectName'] = self.settings['path'].rpartition('/')[2]
        self.settings['directory'] = self.settings['path'].rpartition('/')[0]
        self.setWindowTitle("QualCoder " + self.settings['projectName'])
        cur = self.settings['conn'].cursor()
        cur.execute("select databaseversion, date, memo, about from project")
        result = cur.fetchone()
        self.project['databaseversion'] = result[0]
        self.project['date'] = result[1]
        self.project['memo'] = result[2]
        self.project['about'] = result[3]
        # Save a datetime stamped backup
        nowdate = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
        backup = self.settings['path'][0:-4] + "_BACKUP_" + nowdate + ".qda"
        shutil.copytree(self.settings['path'], backup)
        self.ui.textEdit.append(
            _("Project Opened: ") + self.settings['projectName'] +
            "\n========\n" + _("Path: ") + self.settings['path'] + "\n" +
            _("Directory: ") + self.settings['directory'] + "\n" +
            _("Database version: ") + self.project['databaseversion'] + ". " +
            _("Date: ") + str(self.project['date']) + "\n" + _("About: ") +
            self.project['about'] + "\n" + _("Language: ") +
            self.settings['language'] + "\n" + _("Project backup created: ") +
            backup + "\n========\n")
        self.show_menu_options()

    def close_project(self):
        """ Close an open project. """

        self.ui.textEdit.append("Closing project: " +
                                self.settings['projectName'] + "\n========\n")
        try:
            self.settings['conn'].commit()
            self.settings['conn'].close()
        except:
            pass
        self.conn = None
        self.settings['conn'] = None
        self.settings['path'] = ""
        self.settings['projectName'] = ""
        self.settings['directory'] = ""
        self.project = {
            "databaseversion": "",
            "date": "",
            "memo": "",
            "about": ""
        }
        self.hide_menu_options()
        self.clean_dialog_refs()

    def clean_dialog_refs(self):
        """ Test the list of dialog refs to see if they have been cleared
        and create a new list of current dialogs.
        Also need to keep these dialog references to keep non-modal dialogs open.
        Non-modal example - having a journal open and a coding dialog. """

        tempList = []
        for d in self.dialogList:
            try:
                #logger.debug(str(d) + ", isVisible:" + str(d.isVisible()) + " Title:" + d.windowTitle())
                #d.windowTitle()
                if d.isVisible():
                    tempList.append(d)
            # RuntimeError: wrapped C/C++ object of type DialogSQL has been deleted
            except RuntimeError as e:
                logger.error(str(e))
        self.dialogList = tempList
Пример #5
0
class MainWindow(QtWidgets.QMainWindow):
    ''' Main GUI window.
    Project data is stored in a directory with .qda suffix
    core data is stored in data.qda sqlite file.
    Journal and coding dialogs can beshown non=modally - multiple dialogs open.
    There is a risk of a clash if two coding windows are open with the same file text or
    two journals open with the same journal entry. '''

    settings = {
        "conn": None,
        "directory": home,
        "projectName": "",
        "showIDs": False,
        'path': home,
        "codername": "default",
        "font": "Noto Sans",
        "fontsize": 10,
        'treefontsize': 10
    }
    project = {"databaseversion": "", "date": "", "memo": "", "about": ""}
    dialogList = []  # keeps active and track of non-modal windows

    def __init__(self):
        ''' Set up user interface from ui_main.py file '''

        sys.excepthook = exception_handler
        QtWidgets.QMainWindow.__init__(self)
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.hide_menu_options()
        self.init_ui()
        self.show()

    def init_ui(self):
        ''' set up menu triggers '''

        # project menu
        self.ui.actionCreate_New_Project.triggered.connect(self.new_project)
        self.ui.actionOpen_Project.triggered.connect(self.open_project)
        self.ui.actionProject_Memo.triggered.connect(self.project_memo)
        self.ui.actionClose_Project.triggered.connect(self.close_project)
        self.ui.actionSettings.triggered.connect(self.change_settings)
        self.ui.actionExit.triggered.connect(self.closeEvent)

        # file cases and journals menu
        self.ui.actionManage_files.triggered.connect(self.manage_files)
        self.ui.actionManage_journals.triggered.connect(self.journals)
        self.ui.actionManage_cases.triggered.connect(self.manage_cases)
        self.ui.actionManage_attributes.triggered.connect(
            self.manage_attributes)
        self.ui.actionImport_survey.triggered.connect(self.import_survey)

        # codes menu
        self.ui.actionCodes.triggered.connect(self.text_coding)
        self.ui.actionCode_image.triggered.connect(self.image_coding)
        self.ui.actionExport_codebook.triggered.connect(self.codebook)
        self.ui.actionView_Graph.triggered.connect(self.view_graph)

        # reports menu
        self.ui.actionCoding_reports.triggered.connect(self.report_coding)
        self.ui.actionCoding_comparison.triggered.connect(
            self.report_coding_comparison)
        self.ui.actionCode_frequencies.triggered.connect(
            self.report_code_frequencies)
        #TODO self.ui.actionText_mining.triggered.connect(self.text_mining)
        self.ui.actionSQL_statements.triggered.connect(self.report_sql)

        # help menu
        self.ui.actionContents.triggered.connect(self.help)
        self.ui.actionAbout.triggered.connect(self.about)

        # load_settings from file stored in home
        try:
            with open(home + '/QualCoder_settings.txt') as f:
                txt = f.read()
                txt = txt.split("\n")
                self.settings['codername'] = txt[0]
                self.settings['font'] = txt[1]
                self.settings['fontsize'] = int(txt[2])
                self.settings['treefontsize'] = int(txt[3])
                self.settings['directory'] = txt[4]
                self.settings['showIDs'] = False
                if txt[5] == "True":
                    self.settings['showIDs'] = True
        except:
            f = open(home + '/QualCoder_settings.txt', 'w')
            f.write("default\nNoto Sans\n10\n10\n" + home + "\nFalse\n")
            f.close()
        new_font = QtGui.QFont(self.settings['font'],
                               self.settings['fontsize'], QtGui.QFont.Normal)
        self.setFont(new_font)
        self.settings_report()

    def hide_menu_options(self):
        ''' No project opened, hide options '''

        self.ui.actionClose_Project.setEnabled(False)
        self.ui.actionProject_Memo.setEnabled(False)
        # files cases journals menu
        self.ui.actionManage_files.setEnabled(False)
        self.ui.actionManage_journals.setEnabled(False)
        self.ui.actionManage_cases.setEnabled(False)
        self.ui.actionManage_attributes.setEnabled(False)
        self.ui.actionImport_survey.setEnabled(False)
        # codes menu
        self.ui.actionCodes.setEnabled(False)
        self.ui.actionCode_image.setEnabled(False)
        self.ui.actionCategories.setEnabled(False)
        self.ui.actionView_Graph.setEnabled(False)
        # reports menu
        self.ui.actionCoding_reports.setEnabled(False)
        self.ui.actionCoding_comparison.setEnabled(False)
        self.ui.actionCode_frequencies.setEnabled(False)
        self.ui.actionText_mining.setEnabled(False)
        self.ui.actionSQL_statements.setEnabled(False)

    def show_menu_options(self):
        ''' Project opened, hide options '''

        self.ui.actionClose_Project.setEnabled(True)
        self.ui.actionProject_Memo.setEnabled(True)
        # files cases journals menu
        self.ui.actionManage_files.setEnabled(True)
        self.ui.actionManage_journals.setEnabled(True)
        self.ui.actionManage_cases.setEnabled(True)
        self.ui.actionManage_attributes.setEnabled(True)
        self.ui.actionImport_survey.setEnabled(True)
        # codes menu
        self.ui.actionCodes.setEnabled(True)
        self.ui.actionCode_image.setEnabled(True)
        self.ui.actionCategories.setEnabled(True)
        self.ui.actionView_Graph.setEnabled(True)
        # reports menu
        self.ui.actionCoding_reports.setEnabled(True)
        self.ui.actionCoding_comparison.setEnabled(True)
        self.ui.actionCode_frequencies.setEnabled(True)
        self.ui.actionSQL_statements.setEnabled(True)
        #TODO FOR FUTURE EXPANSION text mining
        self.ui.actionText_mining.setEnabled(False)

    def settings_report(self):
        msg = "Settings\n========\nCoder: " + self.settings['codername'] + "\n"
        msg += "Font: " + self.settings['font'] + " " + str(
            self.settings['fontsize'])
        msg += ". Code font size: " + self.settings['font'] + " " + str(
            self.settings['treefontsize'])
        msg += "\nDirectory: " + self.settings['directory']
        msg += "\nShowIDs: " + str(self.settings['showIDs'])
        msg += "\n========"
        self.ui.textEdit.append(msg)

    def report_sql(self):
        ''' Run SQL statements on database. non-modal. '''

        ui = DialogSQL(self.settings, self.ui.textEdit)
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    """def text_mining(self):
        ''' text analysis of files / cases / codings.
        NOT CURRENTLY IMPLEMENTED, FOR FUTURE EXPANSION.
        '''

        ui = DialogTextMining(self.settings, self.ui.textEdit)
        ui.show()"""

    def report_coding_comparison(self):
        ''' compare two or more coders using Cohens Kappa. non-modal. '''

        ui = DialogReportCoderComparisons(self.settings, self.ui.textEdit)
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    def report_code_frequencies(self):
        ''' show code frequencies overall and by coder. non-modal. '''

        ui = DialogReportCodeFrequencies(self.settings, self.ui.textEdit)
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    def report_coding(self):
        ''' report on coding and categories. non-modal. '''

        ui = DialogReportCodes(self.settings, self.ui.textEdit)
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    def view_graph(self):
        ''' Show acyclic graph of codes and categories. non-modal. '''

        ui = ViewGraph(self.settings)
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    def help(self):
        ''' Help dialog.  non-modal. '''

        ui = DialogInformation("Help contents", "Help.html")
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    def about(self):
        ''' About dialog.  non-modal. '''

        ui = DialogInformation("About", "About.html")
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    def manage_attributes(self):
        ''' Create, edit, delete, rename attributes '''

        ui = DialogManageAttributes(self.settings, self.ui.textEdit)
        ui.exec_()
        self.clean_dialog_refs()

    def import_survey(self):
        ''' Import survey flat sheet: csv file.
        Create cases and assign attributes to cases.
        Identify qualitative questions and assign these data to the source table for coding and
        review '''

        ui = DialogImportSurvey(self.settings, self.ui.textEdit)
        ui.exec_()

    def manage_cases(self):
        ''' Create, edit, delete, rename cases, add cases to files or parts of
        files, add memos to cases '''

        ui = DialogCases(self.settings, self.ui.textEdit)
        ui.exec_()
        self.clean_dialog_refs()

    def manage_files(self):
        ''' Create text files or import files from odt, docx, html and
        plain text. Rename, delete and add memos to files.
        '''

        ui = DialogManageFiles(self.settings, self.ui.textEdit)
        ui.exec_()
        self.clean_dialog_refs()

    def journals(self):
        ''' Create and edit journals. non-modal.
        '''

        ui = DialogJournals(self.settings, self.ui.textEdit)
        ui.setAttribute(QtCore.Qt.WA_DeleteOnClose)
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    def text_coding(self):
        ''' Create edit and delete codes. Apply and remove codes and annotations to the
        text in imported text files. Multiple coding windows can be displayed non-modal.
        '''

        ui = DialogCodeText(self.settings, self.ui.textEdit)
        ui.setAttribute(QtCore.Qt.WA_DeleteOnClose)
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    def image_coding(self):
        ''' Create edit and delete codes. Apply and remove codes to the image (or regions)
        Multiple coding windows can be displayed non-modal.
        '''

        ui = DialogCodeImage(self.settings)
        ui.setAttribute(QtCore.Qt.WA_DeleteOnClose)
        self.dialogList.append(ui)
        ui.show()
        self.clean_dialog_refs()

    def codebook(self):
        ''' Export a code book of categories and codes.
        '''

        Codebook(self.settings, self.ui.textEdit)

    def closeEvent(self, event):
        ''' Override the QWindow close event.
        Close all dialogs and database connection.
        '''

        quit_msg = "Are you sure you want to quit?"
        reply = QtWidgets.QMessageBox.question(self, 'Message', quit_msg,
                                               QtWidgets.QMessageBox.Yes,
                                               QtWidgets.QMessageBox.No)
        if reply == QtWidgets.QMessageBox.Yes:
            self.dialogList = None
            if self.settings['conn'] is not None:
                try:
                    self.settings['conn'].commit()
                    self.settings['conn'].close()
                except:
                    pass
            QtWidgets.qApp.quit()
        else:
            return False
        True

    def new_project(self):
        ''' Create a new project folder with data.qda (sqlite) and folders for documents,
        images and audio '''

        #logger.debug("settings[directory]:" + self.settings['directory'])
        self.settings['path'] = QtWidgets.QFileDialog.getSaveFileName(
            self, "Enter project name", self.settings['directory'], ".qda")[0]
        if self.settings['path'] == "":
            QtWidgets.QMessageBox.warning(None, "Project",
                                          "No project created.")
            return
        if self.settings['path'].find(".qda") == -1:
            self.settings['path'] = self.settings['path'] + ".qda"
        try:
            os.mkdir(self.settings['path'])
            os.mkdir(self.settings['path'] + "/images")
            os.mkdir(self.settings['path'] + "/audio")
            os.mkdir(self.settings['path'] + "/documents")
        except Exception as e:
            logger.critical("Project creation error " + str(e))
            QtWidgets.QMessageBox.warning(
                None, "Project", "No project created.Exiting. " + str(e))
            exit(0)
        self.settings['projectName'] = self.settings['path'].rpartition('/')[2]
        self.settings['directory'] = self.settings['path'].rpartition('/')[0]
        #try:
        self.settings['conn'] = sqlite3.connect(self.settings['path'] +
                                                "/data.qda")
        cur = self.settings['conn'].cursor()
        cur.execute(
            "CREATE TABLE project (databaseversion text, date text, memo text,about text);"
        )
        cur.execute(
            "CREATE TABLE source (id integer primary key, name text, fulltext text, imagepath text, memo text, owner text, date text, unique(name));"
        )
        cur.execute(
            "CREATE TABLE code_image (imid integer primary key,id integer,x1 integer, y1 integer, width integer, height integer, cid integer, memo text, date text, owner text);"
        )
        cur.execute(
            "CREATE TABLE annotation (anid integer primary key, fid integer,pos0 integer, pos1 integer, memo text, owner text, date text);"
        )
        cur.execute(
            "CREATE TABLE attribute_type (name text primary key, date text, owner text, memo text, caseOrFile text, valuetype text);"
        )
        cur.execute(
            "CREATE TABLE attribute (attrid integer primary key, name text, attr_type text, value text, id integer, date text, owner text);"
        )
        cur.execute(
            "CREATE TABLE case_text (id integer primary key, caseid integer, fid integer, pos0 integer, pos1 integer, owner text, date text, memo text);"
        )
        cur.execute(
            "CREATE TABLE cases (caseid integer primary key, name text, memo text, owner text,date text, constraint ucm unique(name));"
        )
        cur.execute(
            "CREATE TABLE code_cat (catid integer primary key, name text, owner text, date text, memo text, supercatid integer, unique(name));"
        )
        cur.execute(
            "CREATE TABLE code_text (cid integer, fid integer,seltext text, pos0 integer, pos1 integer, owner text, date text, memo text, unique(cid,fid,pos0,pos1, owner));"
        )
        cur.execute(
            "CREATE TABLE code_name (cid integer primary key, name text, memo text, catid integer, owner text,date text, color text, unique(name));"
        )
        cur.execute(
            "CREATE TABLE journal (jid integer primary key, name text, jentry text, date text, owner text);"
        )
        cur.execute(
            "INSERT INTO project VALUES(?,?,?,?)",
            ('v1', datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), '',
             'QualCoder 1.0'))
        self.settings['conn'].commit()
        try:
            # get and display some project details
            self.ui.textEdit.append("\nNew project: " + self.settings['path'] +
                                    " created.")
            #self.settings['projectName'] = self.path.rpartition('/')[2]
            self.ui.textEdit.append("Opening: " + self.settings['path'])
            self.setWindowTitle("QualCoder " + self.settings['projectName'])
            cur = self.settings['conn'].cursor()
            cur.execute('select sqlite_version()')
            self.ui.textEdit.append("SQLite version: " + str(cur.fetchone()))
            cur.execute(
                "select databaseversion, date, memo, about from project")
            result = cur.fetchone()
            self.project['databaseversion'] = result[0]
            self.project['date'] = result[1]
            self.project['memo'] = result[2]
            self.project['about'] = result[3]
            self.ui.textEdit.append("New Project Created\n========\n" +
                                    "DBVersion:" +
                                    str(self.project['databaseversion']) +
                                    "\n" + "Date: " +
                                    str(self.project['date']) + "\n" +
                                    "About: " + str(self.project['about']) +
                                    "\n" + "Coder:" +
                                    str(self.settings['codername']) + "\n" +
                                    "========")
        except Exception as e:
            logger.warning("Problem creating Db " + self.settings['path'] +
                           " Exception:" + str(e))
            self.ui.textEdit.append("\nProblems creating Db\n" +
                                    self.settings['path'])
            self.ui.textEdit.append(str(e))
            self.close_project()
            return
        self.open_project(self.settings['path'])

    def change_settings(self):
        ''' Change default settings - the coder name, font, font size '''

        ui = DialogSettings(self.settings)
        ui.exec_()
        self.settings_report()
        newfont = QtGui.QFont(self.settings['font'], self.settings['fontsize'],
                              QtGui.QFont.Normal)
        self.setFont(newfont)

    def project_memo(self):
        ''' Give the entire project a memo '''

        cur = self.settings['conn'].cursor()
        cur.execute("select memo from project")
        memo = cur.fetchone()[0]
        ui = DialogMemo(self.settings,
                        "Memo for project " + self.settings['projectName'],
                        memo)
        ui.exec_()
        if memo != ui.memo:
            cur.execute('update project set memo=?', (ui.memo, ))
            self.settings['conn'].commit()
            self.ui.textEdit.append("Project memo entered.")

    def open_project(self, path=""):
        ''' Open an existing project. '''

        if self.settings['projectName'] != "":
            self.close_project()
        self.setWindowTitle("QualCoder Open Project")
        if path == "" or path is False:
            path = QtWidgets.QFileDialog.getExistingDirectory(
                self, 'Open project directory', self.settings['directory'])
        if path == "" or path is False:
            return
        if len(path) > 3 and path[-4:] == ".qda":
            self.settings['path'] = path
            msg = ""
            try:
                self.settings['conn'] = sqlite3.connect(self.settings['path'] +
                                                        "/data.qda")
            except Exception as e:
                self.settings['conn'] = None
                msg += str(e)
                logger.debug(str(e))
        if self.settings['conn'] is None:
            QtWidgets.QMessageBox.warning(
                None, "Cannot open file",
                self.settings['path'] + " is not a .qda file ")
            self.settings['path'] = ""
            return
        # get and display some project details
        self.settings['path'] = path
        self.settings['projectName'] = self.settings['path'].rpartition('/')[2]
        self.settings['directory'] = self.settings['path'].rpartition('/')[0]
        self.setWindowTitle("QualCoder " + self.settings['projectName'])
        cur = self.settings['conn'].cursor()
        cur.execute("select databaseversion, date, memo, about from project")
        result = cur.fetchone()
        self.project['databaseversion'] = result[0]
        self.project['date'] = result[1]
        self.project['memo'] = result[2]
        self.project['about'] = result[3]
        #self.settings_report()
        self.ui.textEdit.append("Project Opened:" +
                                self.settings['projectName'] + "\n========" +
                                "\nPath: " + self.settings['path'] +
                                "\nDirectory: " + self.settings['directory'] +
                                "\nDBVersion:" +
                                self.project['databaseversion'] + ". " +
                                "Date: " + str(self.project['date']) + "\n" +
                                "About: " + self.project['about'] +
                                "\n========\n")
        self.show_menu_options()

    def close_project(self):
        ''' Close an open project '''

        self.ui.textEdit.append("Closing project: " +
                                self.settings['projectName'] + "\n========\n")
        try:
            self.settings['conn'].commit()
            self.settings['conn'].close()
        except:
            pass
        self.conn = None
        self.settings['conn'] = None
        self.settings['path'] = ""
        self.settings['projectName'] = ""
        self.settings['directory'] = ""
        self.project = {
            "databaseversion": "",
            "date": "",
            "memo": "",
            "about": ""
        }
        self.hide_menu_options()
        self.clean_dialog_refs()

    def clean_dialog_refs(self):
        ''' Test the list of dialog refs to see if they have been cleared
        and create a new list of current dialogs.
        Also need to keep these dialog references to keep non-modal dialogs open.
        Non-modal example - having a journal open and a coding dialog '''

        tempList = []
        for d in self.dialogList:
            try:
                #logger.debug(str(d) + ", isVisible:" + str(d.isVisible()) + " Title:" + d.windowTitle())
                d.windowTitle()
                tempList.append(d)
            # RuntimeError: wrapped C/C++ object of type DialogSQL has been deleted
            except RuntimeError as e:
                logger.error(str(e))
        self.dialogList = tempList