예제 #1
0
class TrackmaWindow(Gtk.ApplicationWindow):
    __gtype_name__ = 'TrackmaWindow'

    btn_appmenu = Gtk.Template.Child()
    btn_mediatype = Gtk.Template.Child()
    header_bar = Gtk.Template.Child()

    def __init__(self, app, debug=False):
        Gtk.ApplicationWindow.__init__(self, application=app)
        self.init_template()

        self._debug = debug
        self._configfile = utils.to_config_path('ui-Gtk.json')
        self._config = utils.parse_config(self._configfile, utils.gtk_defaults)

        self.statusicon = None
        self._main_view = None
        self._modals = []

        self._account = None
        self._engine = None
        self.close_thread = None
        self.hidden = False

        self._init_widgets()

    def init_account_selection(self):
        manager = AccountManager()

        # Use the remembered account if there's one
        if manager.get_default():
            self._create_engine(manager.get_default())
        else:
            self._show_accounts(switch=False)

    def _init_widgets(self):
        Gtk.Window.set_default_icon_from_file(utils.DATADIR + '/icon.png')
        self.set_position(Gtk.WindowPosition.CENTER)
        self.set_title('Trackma')

        if self._config['remember_geometry']:
            self.resize(self._config['last_width'],
                        self._config['last_height'])

        if not self._main_view:
            self._main_view = MainView(self._config)
            self._main_view.connect('error', self._on_main_view_error)
            self._main_view.connect(
                'success', lambda x: self._set_buttons_sensitive(True))
            self._main_view.connect('error-fatal',
                                    self._on_main_view_error_fatal)
            self._main_view.connect('show-action', self._on_show_action)
            self.add(self._main_view)

        self.connect('delete_event', self._on_delete_event)

        builder = Gtk.Builder.new_from_file(
            os.path.join(gtk_dir, 'data/shortcuts.ui'))
        help_overlay = builder.get_object('shortcuts-window')
        self.set_help_overlay(help_overlay)

        # Status icon
        if TrackmaStatusIcon.is_tray_available():
            self.statusicon = TrackmaStatusIcon()
            self.statusicon.connect('hide-clicked', self._on_tray_hide_clicked)
            self.statusicon.connect('about-clicked',
                                    self._on_tray_about_clicked)
            self.statusicon.connect('quit-clicked', self._on_tray_quit_clicked)

            if self._config['show_tray']:
                self.statusicon.set_visible(True)
            else:
                self.statusicon.set_visible(False)

        # Don't show the main window if start in tray option is set
        if self.statusicon and self._config['show_tray'] and self._config[
                'start_in_tray']:
            self.hidden = True
        else:
            self.present()

    def _on_tray_hide_clicked(self, status_icon):
        self._destroy_modals()

        if self.hidden:
            self.deiconify()
            self.present()

            if not self._engine:
                self._show_accounts(switch=False)
        else:
            self.hide()

        self.hidden = not self.hidden

    def _destroy_modals(self):
        self.get_help_overlay().hide()

        for modal_window in self._modals:
            modal_window.destroy()

        self._modals = []

    def _on_tray_about_clicked(self, status_icon):
        self._on_about(None, None)

    def _on_tray_quit_clicked(self, status_icon):
        self._quit()

    def _on_delete_event(self, widget, event, data=None):
        if self.statusicon and self.statusicon.get_visible(
        ) and self._config['close_to_tray']:
            self.hidden = True
            self.hide()
        else:
            self._quit()
        return True

    def _create_engine(self, account):
        self._engine = Engine(account, self._message_handler)

        self._main_view.load_engine_account(self._engine, account)
        self._set_actions()
        self._set_mediatypes_menu()
        self._update_widgets(account)
        self._set_buttons_sensitive(True)

    def _set_actions(self):
        builder = Gtk.Builder.new_from_file(
            os.path.join(gtk_dir, 'data/app-menu.ui'))
        settings = Gtk.Settings.get_default()
        if not settings.get_property("gtk-shell-shows-menubar"):
            self.btn_appmenu.set_menu_model(builder.get_object('app-menu'))
        else:
            self.get_application().set_menubar(builder.get_object('menu-bar'))
            self.btn_appmenu.set_property('visible', False)

        def add_action(name, callback):
            action = Gio.SimpleAction.new(name, None)
            action.connect('activate', callback)
            self.add_action(action)

        add_action('search', self._on_search)
        add_action('syncronize', self._on_synchronize)
        add_action('upload', self._on_upload)
        add_action('download', self._on_download)
        add_action('scanfiles', self._on_scanfiles)
        add_action('accounts', self._on_accounts)
        add_action('preferences', self._on_preferences)
        add_action('about', self._on_about)

        add_action('play_next', self._on_action_play_next)
        add_action('play_random', self._on_action_play_random)
        add_action('episode_add', self._on_action_episode_add)
        add_action('episode_remove', self._on_action_episode_remove)
        add_action('delete', self._on_action_delete)
        add_action('copy', self._on_action_copy)

    def _set_mediatypes_action(self):
        action_name = 'change-mediatype'
        if self.has_action(action_name):
            self.remove_action(action_name)

        state = GLib.Variant.new_string(self._engine.api_info['mediatype'])
        action = Gio.SimpleAction.new_stateful(action_name, state.get_type(),
                                               state)
        action.connect('change-state', self._on_change_mediatype)
        self.add_action(action)

    def _set_mediatypes_menu(self):
        self._set_mediatypes_action()
        menu = Gio.Menu()

        for mediatype in self._engine.api_info['supported_mediatypes']:
            variant = GLib.Variant.new_string(mediatype)
            menu_item = Gio.MenuItem()
            menu_item.set_label(mediatype)
            menu_item.set_action_and_target_value('win.change-mediatype',
                                                  variant)
            menu.append_item(menu_item)

        self.btn_mediatype.set_menu_model(menu)

        if len(self._engine.api_info['supported_mediatypes']) <= 1:
            self.btn_mediatype.hide()

    def _update_widgets(self, account):
        current_api = utils.available_libs[account['api']]
        api_iconpath = 1
        api_iconfile = current_api[api_iconpath]

        self.header_bar.set_subtitle(self._engine.api_info['name'] + " (" +
                                     self._engine.api_info['mediatype'] + ")")

        if self.statusicon and self._config['tray_api_icon']:
            self.statusicon.set_from_file(api_iconfile)

    def _on_change_mediatype(self, action, value):
        action.set_state(value)
        mediatype = value.get_string()
        self._set_buttons_sensitive(False)
        self._main_view.load_account_mediatype(None, mediatype,
                                               self.header_bar)

    def _on_search(self, action, param):
        current_status = self._main_view.get_current_status()
        win = SearchWindow(self._engine,
                           self._config['colors'],
                           current_status,
                           transient_for=self)
        win.connect('search-error', self._on_search_error)
        win.connect('destroy', self._on_modal_destroy)
        win.present()
        self._modals.append(win)

    def _on_search_error(self, search_window, error_msg):
        print(error_msg)

    def _on_synchronize(self, action, param):
        threading.Thread(target=self._synchronization_task,
                         args=(True, True)).start()

    def _on_upload(self, action, param):
        threading.Thread(target=self._synchronization_task,
                         args=(True, False)).start()

    def _on_download(self, action, param):
        def _download_lists():
            threading.Thread(target=self._synchronization_task,
                             args=(False, True)).start()

        def _on_download_response(_dialog, response):
            _dialog.destroy()

            if response == Gtk.ResponseType.YES:
                _download_lists()

        queue = self._engine.get_queue()
        if queue:
            dialog = Gtk.MessageDialog(
                self, Gtk.DialogFlags.MODAL, Gtk.MessageType.QUESTION,
                Gtk.ButtonsType.YES_NO,
                "There are %d queued changes in your list. If you retrieve the remote list now you will lose your queued changes. Are you sure you want to continue?"
                % len(queue))
            dialog.show_all()
            dialog.connect("response", _on_download_response)
        else:
            # If the user doesn't have any queued changes
            # just go ahead
            _download_lists()

    def _synchronization_task(self, send, retrieve):
        self._set_buttons_sensitive_idle(False)

        try:
            if send:
                self._engine.list_upload()
            if retrieve:
                self._engine.list_download()

            # GLib.idle_add(self._set_score_ranges)
            GLib.idle_add(self._main_view.populate_all_pages)
        except utils.TrackmaError as e:
            self._error_dialog_idle(e)
        except utils.TrackmaFatal as e:
            self._show_accounts_idle(switch=False, forget=True)
            self._error_dialog_idle("Fatal engine error: %s" % e)
            return

        self._main_view.set_status_idle("Ready.")
        self._set_buttons_sensitive_idle(True)

    def _on_scanfiles(self, action, param):
        threading.Thread(target=self._scanfiles_task).start()

    def _scanfiles_task(self):
        self._set_buttons_sensitive_idle(False)
        try:
            self._engine.scan_library(rescan=True)
        except utils.TrackmaError as e:
            self._error_dialog_idle(e)

        GLib.idle_add(self._main_view.populate_all_pages)

        self._main_view.set_status_idle("Ready.")
        self._set_buttons_sensitive_idle(True)

    def _on_accounts(self, action, param):
        self._show_accounts()

    def _show_accounts_idle(self, switch=True, forget=False):
        GLib.idle_add(self._show_accounts, switch, forget)

    def _show_accounts(self, switch=True, forget=False):
        manager = AccountManager()

        if forget:
            manager.set_default(None)

        accountsel = AccountsWindow(manager, transient_for=self)
        accountsel.connect('account-open', self._on_account_open)
        accountsel.connect('account-cancel', self._on_account_cancel, switch)
        accountsel.connect('destroy', self._on_modal_destroy)
        accountsel.present()
        self._modals.append(accountsel)

    def _on_account_open(self, accounts_window, account_num, remember):
        manager = AccountManager()
        account = manager.get_account(account_num)

        if remember:
            manager.set_default(account_num)
        else:
            manager.set_default(None)

        # Reload the engine if already started,
        # start it otherwise
        self._set_buttons_sensitive(False)
        if self._engine and self._engine.loaded:
            self._main_view.load_account_mediatype(account, None, None)
        else:
            self._create_engine(account)

    def _on_account_cancel(self, _accounts_window, switch):
        manager = AccountManager()

        if not switch or not manager.get_accounts():
            self._quit()

    def _on_preferences(self, _action, _param):
        win = SettingsWindow(self._engine,
                             self._config,
                             self._configfile,
                             transient_for=self)
        win.connect('destroy', self._on_modal_destroy)
        win.present()
        self._modals.append(win)

    def _on_about(self, _action, _param):
        about = Gtk.AboutDialog(parent=self)
        about.set_modal(True)
        about.set_transient_for(self)
        about.set_program_name("Trackma GTK")
        about.set_version(utils.VERSION)
        about.set_license_type(Gtk.License.GPL_3_0_ONLY)
        about.set_comments(
            "Trackma is an open source client for media tracking websites.\nThanks to all contributors."
        )
        about.set_website("http://github.com/z411/trackma")
        about.set_copyright("© z411, et al.")
        about.set_authors(["See AUTHORS file"])
        about.set_artists(["shuuichi"])
        about.connect('destroy', self._on_modal_destroy)
        about.connect('response', lambda dialog, response: dialog.destroy())
        about.present()
        self._modals.append(about)

    def _on_modal_destroy(self, modal_window):
        self._modals.remove(modal_window)

    def _quit(self):
        if self._config['remember_geometry']:
            self._store_geometry()

        if not self._engine:
            self.get_application().quit()
            return

        if self.close_thread is None:
            self._set_buttons_sensitive_idle(False)
            self.close_thread = threading.Thread(target=self._unload_task)
            self.close_thread.start()

    def _unload_task(self):
        self._engine.unload()
        GLib.idle_add(self.get_application().quit)

    def _store_geometry(self):
        (width, height) = self.get_size()
        self._config['last_width'] = width
        self._config['last_height'] = height
        utils.save_config(self._config, self._configfile)

    def _message_handler(self, classname, msgtype, msg):
        # Thread safe
        # print("%s: %s" % (classname, msg))
        if msgtype == messenger.TYPE_WARN:
            self._main_view.set_status_idle("%s warning: %s" %
                                            (classname, msg))
        elif msgtype != messenger.TYPE_DEBUG:
            self._main_view.set_status_idle("%s: %s" % (classname, msg))
        elif self._debug:
            print('[D] {}: {}'.format(classname, msg))

    def _on_main_view_error(self, main_view, error_msg):
        self._error_dialog_idle(error_msg)

    def _on_main_view_error_fatal(self, main_view, error_msg):
        self._show_accounts_idle(switch=False, forget=True)
        self._error_dialog_idle(error_msg)

    def _error_dialog_idle(self, msg, icon=Gtk.MessageType.ERROR):
        # Thread safe
        GLib.idle_add(self._error_dialog, msg, icon)

    def _error_dialog(self, msg, icon=Gtk.MessageType.ERROR):
        def error_dialog_response(widget, response_id):
            widget.destroy()

        dialog = Gtk.MessageDialog(self, Gtk.DialogFlags.MODAL, icon,
                                   Gtk.ButtonsType.OK, str(msg))
        dialog.show_all()
        dialog.connect("response", error_dialog_response)
        print('Error: {}'.format(msg))

    def _on_action_play_next(self, action, param):
        selected_show = self._main_view.get_selected_show()

        if selected_show:
            self._play_next(selected_show)

    def _on_action_play_random(self, action, param):
        self._play_random()

    def _on_action_episode_add(self, action, param):
        selected_show = self._main_view.get_selected_show()

        if selected_show:
            self._episode_add(selected_show)

    def _on_action_episode_remove(self, action, param):
        selected_show = self._main_view.get_selected_show()

        if selected_show:
            self._episode_remove(selected_show)

    def _on_action_delete(self, action, param):
        selected_show = self._main_view.get_selected_show()

        if selected_show:
            self._remove_show(selected_show)

    def _on_action_copy(self, action, param):
        selected_show = self._main_view.get_selected_show()

        if selected_show:
            self._copy_title(selected_show)

    def _on_show_action(self, main_view, event_type, data):
        if event_type == ShowEventType.PLAY_NEXT:
            self._play_next(*data)
        elif event_type == ShowEventType.PLAY_EPISODE:
            self._play_episode(*data)
        elif event_type == ShowEventType.EPISODE_REMOVE:
            self._episode_remove(*data)
        elif event_type == ShowEventType.EPISODE_SET:
            self._episode_set(*data)
        elif event_type == ShowEventType.EPISODE_ADD:
            self._episode_add(*data)
        elif event_type == ShowEventType.SET_SCORE:
            self._set_score(*data)
        elif event_type == ShowEventType.SET_STATUS:
            self._set_status(*data)
        elif event_type == ShowEventType.DETAILS:
            self._open_details(*data)
        elif event_type == ShowEventType.OPEN_WEBSITE:
            self._open_website(*data)
        elif event_type == ShowEventType.OPEN_FOLDER:
            self._open_folder(*data)
        elif event_type == ShowEventType.COPY_TITLE:
            self._copy_title(*data)
        elif event_type == ShowEventType.CHANGE_ALTERNATIVE_TITLE:
            self._change_alternative_title(*data)
        elif event_type == ShowEventType.REMOVE:
            self._remove_show(*data)

    def _play_next(self, show_id):
        show = self._engine.get_show_info(show_id)
        try:
            args = self._engine.play_episode(show)
            utils.spawn_process(args)
        except utils.TrackmaError as e:
            self._error_dialog(e)

    def _play_episode(self, show_id, episode):
        show = self._engine.get_show_info(show_id)
        try:
            if not episode:
                episode = self.show_ep_num.get_value_as_int()
            args = self._engine.play_episode(show, episode)
            utils.spawn_process(args)
        except utils.TrackmaError as e:
            self._error_dialog(e)

    def _play_random(self):
        try:
            args = self._engine.play_random()
            utils.spawn_process(args)
        except utils.TrackmaError as e:
            self._error_dialog(e)

    def _episode_add(self, show_id):
        show = self._engine.get_show_info(show_id)
        self._episode_set(show_id, show['my_progress'] + 1)

    def _episode_remove(self, show_id):
        show = self._engine.get_show_info(show_id)
        self._episode_set(show_id, show['my_progress'] - 1)

    def _episode_set(self, show_id, episode):
        try:
            self._engine.set_episode(show_id, episode)
        except utils.TrackmaError as e:
            self._error_dialog(e)

    def _set_score(self, show_id, score):
        try:
            self._engine.set_score(show_id, score)
        except utils.TrackmaError as e:
            self._error_dialog(e)

    def _set_status(self, show_id, status):
        try:
            self._engine.set_status(show_id, status)
        except utils.TrackmaError as e:
            self._error_dialog(e)

    def _open_details(self, show_id):
        show = self._engine.get_show_info(show_id)
        win = ShowInfoWindow(self._engine, show, transient_for=self)
        win.connect('destroy', self._on_modal_destroy)
        win.present()
        self._modals.append(win)

    def _open_website(self, show_id):
        show = self._engine.get_show_info(show_id)
        if show['url']:
            Gtk.show_uri(None, show['url'], Gdk.CURRENT_TIME)

    def _open_folder(self, show_id):
        show = self._engine.get_show_info(show_id)
        try:
            filename = self._engine.get_episode_path(show, 1)
            with open(os.devnull, 'wb') as DEVNULL:
                if sys.platform == 'darwin':
                    subprocess.Popen(
                        ["open", os.path.dirname(filename)],
                        stdout=DEVNULL,
                        stderr=DEVNULL)
                elif sys.platform == 'win32':
                    subprocess.Popen(
                        ["explorer", os.path.dirname(filename)],
                        stdout=DEVNULL,
                        stderr=DEVNULL)
                else:
                    subprocess.Popen(
                        ["/usr/bin/xdg-open",
                         os.path.dirname(filename)],
                        stdout=DEVNULL,
                        stderr=DEVNULL)
        except OSError:
            # xdg-open failed.
            raise utils.EngineError("Could not open folder.")

        except utils.EngineError:
            # Show not in library.
            self._error_dialog_idle("No folder found.")

    def _copy_title(self, show_id):
        show = self._engine.get_show_info(show_id)
        clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
        clipboard.set_text(show['title'], -1)

        self._main_view.set_status_idle('Title copied to clipboard.')

    def _change_alternative_title(self, show_id):
        show = self._engine.get_show_info(show_id)
        current_altname = self._engine.altname(show_id)

        def altname_response(entry, dialog, response):
            dialog.response(response)

        dialog = Gtk.MessageDialog(
            self, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
            Gtk.MessageType.QUESTION, Gtk.ButtonsType.OK_CANCEL, None)
        dialog.set_markup('Set the <b>alternate title</b> for the show.')
        entry = Gtk.Entry()
        entry.set_text(current_altname)
        entry.connect("activate", altname_response, dialog,
                      Gtk.ResponseType.OK)
        hbox = Gtk.HBox()
        hbox.pack_start(Gtk.Label("Alternate Title:"), False, 5, 5)
        hbox.pack_end(entry, True, True, 0)
        dialog.format_secondary_markup(
            "Use this if the tracker is unable to find this show. Leave blank to disable."
        )
        dialog.vbox.pack_end(hbox, True, True, 0)
        dialog.show_all()
        retval = dialog.run()

        if retval == Gtk.ResponseType.OK:
            text = entry.get_text()
            self._engine.altname(show_id, text)
            self._main_view.change_show_title_idle(show, text)

        dialog.destroy()

    def _remove_show(self, show_id):
        try:
            show = self._engine.get_show_info(show_id)
            self._engine.delete_show(show)
        except utils.TrackmaError as e:
            self._error_dialog_idle(e)

    def _set_buttons_sensitive_idle(self, sensitive):
        GLib.idle_add(self._set_buttons_sensitive, sensitive)
        self._main_view.set_buttons_sensitive_idle(sensitive)

    def _set_buttons_sensitive(self, sensitive):
        actions_names = [
            'search', 'syncronize', 'upload', 'download', 'scanfiles',
            'accounts', 'play_next', 'play_random', 'episode_add',
            'episode_remove', 'delete', 'copy'
        ]

        for action_name in actions_names:
            action = self.lookup_action(action_name)

            if action is not None:
                action.set_enabled(sensitive)
예제 #2
0
class TrackmaWindow(Gtk.ApplicationWindow):
    __gtype_name__ = 'TrackmaWindow'

    btn_appmenu = GtkTemplate.Child()
    btn_mediatype = GtkTemplate.Child()

    _config = None
    show_lists = dict()
    image_thread = None
    close_thread = None
    hidden = False
    quit = False

    statusicon = None

    def __init__(self, debug=False):
        Gtk.ApplicationWindow.__init__(self)
        self.init_template()

        self._debug = debug
        self._configfile = utils.to_config_path('ui-Gtk.json')
        self._config = utils.parse_config(self._configfile, utils.gtk_defaults)

        self._main_view = None
        self._account = None
        self._engine = None

        self._init_widgets()
        self.present()

    def main(self):
        """Start the Account Selector"""
        manager = AccountManager()

        # Use the remembered account if there's one
        if manager.get_default():
            self._create_engine(manager.get_default())
        else:
            self._show_accounts(switch=False)

    def _init_widgets(self):
        Gtk.Window.set_default_icon_from_file(utils.DATADIR + '/icon.png')
        self.set_position(Gtk.WindowPosition.CENTER)
        self.set_title('Trackma-gtk ' + utils.VERSION)

        if self._config['remember_geometry']:
            self.resize(self._config['last_width'],
                        self._config['last_height'])

        if not self._main_view:
            self._main_view = MainView(self._config)
            self._main_view.connect('error', self._on_main_view_error)
            self._main_view.connect('error-fatal',
                                    self._on_main_view_error_fatal)
            self._main_view.connect('show-action', self._on_show_action)
            self.add(self._main_view)

        self.connect('delete_event', self._on_delete_event)
        self.connect('destroy', self._on_destroy)

        # Status icon
        if tray_available:
            self.statusicon = Gtk.StatusIcon()
            self.statusicon.set_from_file(utils.DATADIR + '/icon.png')
            self.statusicon.set_tooltip_text('Trackma-gtk ' + utils.VERSION)
            self.statusicon.connect('activate', self._tray_status_event)
            self.statusicon.connect('popup-menu', self._tray_status_menu_event)
            if self._config['show_tray']:
                self.statusicon.set_visible(True)
            else:
                self.statusicon.set_visible(False)

    def _on_delete_event(self, widget, event, data=None):
        if self.statusicon and self.statusicon.get_visible(
        ) and self._config['close_to_tray']:
            self.hidden = True
            self.hide()
        else:
            self._quit()
        return True

    def _on_destroy(self, widget):
        if self.quit:
            Gtk.main_quit()

    def _create_engine(self, account):
        self._engine = Engine(account, self._message_handler)

        self._main_view.load_engine_account(self._engine, account)
        self._set_actions()
        self._set_mediatypes_menu()
        self._update_widgets(account)

    def _set_actions(self):
        builder = Gtk.Builder.new_from_file(
            os.path.join(gtk_dir, 'data/app-menu.ui'))
        self.btn_appmenu.set_menu_model(builder.get_object('app-menu'))

        def add_action(name, callback):
            action = Gio.SimpleAction.new(name, None)
            action.connect('activate', callback)
            self.add_action(action)

        add_action('search', self._on_search)
        add_action('syncronize', self._on_synchronize)
        add_action('upload', self._on_upload)
        add_action('download', self._on_download)
        add_action('scanfiles', self._on_scanfiles)
        add_action('accounts', self._on_accounts)
        add_action('preferences', self._on_preferences)
        add_action('about', self._on_about)
        add_action('quit', self._on_quit)

    def _set_mediatypes_action(self):
        action_name = 'change-mediatype'
        if self.has_action(action_name):
            self.remove_action(action_name)

        state = GLib.Variant.new_string(self._engine.api_info['mediatype'])
        action = Gio.SimpleAction.new_stateful(action_name, state.get_type(),
                                               state)
        action.connect('change-state', self._on_change_mediatype)
        self.add_action(action)

    def _set_mediatypes_menu(self):
        self._set_mediatypes_action()
        menu = Gio.Menu()

        for mediatype in self._engine.api_info['supported_mediatypes']:
            variant = GLib.Variant.new_string(mediatype)
            menu_item = Gio.MenuItem()
            menu_item.set_label(mediatype)
            menu_item.set_action_and_target_value('win.change-mediatype',
                                                  variant)
            menu.append_item(menu_item)

        self.btn_mediatype.set_menu_model(menu)

        if len(self._engine.api_info['supported_mediatypes']) <= 1:
            self.btn_mediatype.hide()

    def _update_widgets(self, account):
        current_api = utils.available_libs[account['api']]
        api_iconpath = 1
        api_iconfile = current_api[api_iconpath]

        self.set_title('Trackma-gtk %s [%s (%s)]' %
                       (utils.VERSION, self._engine.api_info['name'],
                        self._engine.api_info['mediatype']))

        if self.statusicon and self._config['tray_api_icon']:
            self.statusicon.set_from_file(api_iconfile)

        # Don't show the main dialog if start in tray option is set
        if self.statusicon and self._config['show_tray'] and self._config[
                'start_in_tray']:
            self.hidden = True
        else:
            self.show()

    def _on_change_mediatype(self, action, value):
        action.set_state(value)
        mediatype = value.get_string()
        self._main_view.load_account_mediatype(None, mediatype)

    def _on_search(self, action, param):
        current_status = self._main_view.get_current_status()
        win = SearchWindow(self._engine,
                           self._config['colors'],
                           current_status,
                           transient_for=self)
        win.connect('search-error', self._on_search_error)
        win.show_all()

    def _on_search_error(self, search_window, error_msg):
        print(error_msg)

    def _on_synchronize(self, action, param):
        threading.Thread(target=self._synchronization_task,
                         args=(True, True)).start()

    def _on_upload(self, action, param):
        threading.Thread(target=self._synchronization_task,
                         args=(True, False)).start()

    def _on_download(self, action, param):
        def _download_lists():
            threading.Thread(target=self._synchronization_task,
                             args=(False, True)).start()

        def _on_download_response(_dialog, response):
            _dialog.destroy()

            if response == Gtk.ResponseType.YES:
                _download_lists()

        queue = self._engine.get_queue()
        if not queue:
            dialog = Gtk.MessageDialog(
                self, Gtk.DialogFlags.MODAL, Gtk.MessageType.QUESTION,
                Gtk.ButtonsType.YES_NO,
                "There are %d queued changes in your list. If you retrieve the remote list now you will lose your queued changes. Are you sure you want to continue?"
                % len(queue))
            dialog.show_all()
            dialog.connect("response", _on_download_response)
        else:
            # If the user doesn't have any queued changes
            # just go ahead
            _download_lists()

    def _synchronization_task(self, send, retrieve):
        self._main_view.set_buttons_sensitive_idle(False)

        try:
            if send:
                self._engine.list_upload()
            if retrieve:
                self._engine.list_download()

            # GLib.idle_add(self._set_score_ranges)
            GLib.idle_add(self._main_view.populate_all_pages)
        except utils.TrackmaError as e:
            self._error_dialog_idle(e)
        except utils.TrackmaFatal as e:
            self._show_accounts_idle(switch=False, forget=True)
            self._error_dialog_idle("Fatal engine error: %s" % e)
            return

        self._main_view.set_status_idle("Ready.")
        self._main_view.set_buttons_sensitive_idle(True)

    def _on_scanfiles(self, action, param):
        threading.Thread(target=self._scanfiles_task).start()

    def _scanfiles_task(self):
        try:
            self._engine.scan_library(rescan=True)
        except utils.TrackmaError as e:
            self._error_dialog_idle(e)

        GLib.idle_add(self._main_view.populate_page,
                      self._engine.mediainfo['status_start'])

        self._main_view.set_status_idle("Ready.")
        self._main_view.set_buttons_sensitive_idle(True)

    def _on_accounts(self, action, param):
        self._show_accounts()

    def _show_accounts_idle(self, switch=True, forget=False):
        GLib.idle_add(self._show_accounts, switch, forget)

    def _show_accounts(self, switch=True, forget=False):
        manager = AccountManager()

        if forget:
            manager.set_default(None)

        def _on_accountsel_cancel(accounts_window):
            Gtk.main_quit()

        accountsel = AccountsWindow(manager, transient_for=self)
        accountsel.connect('account-open', self._on_account_open)

        if not switch:
            accountsel.connect('account-cancel', _on_accountsel_cancel)

    def _on_account_open(self, accounts_window, account_num, remember):
        manager = AccountManager()
        account = manager.get_account(account_num)

        if remember:
            manager.set_default(account_num)
        else:
            manager.set_default(None)

        # Reload the engine if already started,
        # start it otherwise
        if self._engine and self._engine.loaded:
            self._main_view.load_account_mediatype(account, None)
        else:
            self._create_engine(account)

    def _on_preferences(self, action, param):
        win = SettingsWindow(self._engine,
                             self._config,
                             self._configfile,
                             transient_for=self)
        win.show_all()

    def _on_about(self, action, param):
        about = Gtk.AboutDialog(parent=self)
        about.set_program_name("Trackma-gtk")
        about.set_version(utils.VERSION)
        about.set_license_type(Gtk.License.GPL_3_0_ONLY)
        about.set_comments(
            "Trackma is an open source client for media tracking websites.\nThanks to all contributors."
        )
        about.set_website("http://github.com/z411/trackma")
        about.set_copyright("© z411, et al.")
        about.set_authors(["See AUTHORS file"])
        about.set_artists(["shuuichi"])
        about.run()
        about.destroy()

    def _on_quit(self, action, param):
        self._quit()

    def _quit(self):
        if self._config['remember_geometry']:
            self._store_geometry()
        if self.close_thread is None:
            self._main_view.set_buttons_sensitive_idle(False)
            self.close_thread = threading.Thread(target=self._unload_task)
            self.close_thread.start()

    def _unload_task(self):
        self._engine.unload()
        self._destroy_idle()

    def _destroy_idle(self):
        GLib.idle_add(self._destroy_push)

    def _destroy_push(self):
        self.quit = True
        self.destroy()

    def _store_geometry(self):
        (width, height) = self.get_size()
        self._config['last_width'] = width
        self._config['last_height'] = height
        utils.save_config(self._config, self._configfile)

    def _message_handler(self, classname, msgtype, msg):
        # Thread safe
        # print("%s: %s" % (classname, msg))
        if msgtype == messenger.TYPE_WARN:
            self._main_view.set_status_idle("%s warning: %s" %
                                            (classname, msg))
        elif msgtype != messenger.TYPE_DEBUG:
            self._main_view.set_status_idle("%s: %s" % (classname, msg))
        elif self._debug:
            print('[D] {}: {}'.format(classname, msg))

    def _on_main_view_error(self, main_view, error_msg):
        self._error_dialog_idle(error_msg)

    def _on_main_view_error_fatal(self, main_view, error_msg):
        self._show_accounts_idle(switch=False, forget=True)
        self._error_dialog_idle(error_msg)

    def _column_toggled(self, w, column_name, visible):
        if visible:
            # Make column visible
            self._config['visible_columns'].append(column_name)

            for view in self.show_lists.values():
                view.cols[column_name].set_visible(True)
        else:
            # Make column invisible
            if len(self._config['visible_columns']) <= 1:
                return  # There should be at least 1 column visible

            self._config['visible_columns'].remove(column_name)
            for view in self.show_lists.values():
                view.cols[column_name].set_visible(False)

        utils.save_config(self._config, self._configfile)

    def _tray_status_event(self, widget):
        # Called when the tray icon is left-clicked
        if self.hidden:
            self.show()
            self.hidden = False
        else:
            self.hide()
            self.hidden = True

    def _tray_status_menu_event(self, icon, button, time):
        # Called when the tray icon is right-clicked
        menu = Gtk.Menu()
        mb_show = Gtk.MenuItem("Show/Hide")
        mb_about = Gtk.ImageMenuItem(
            'About', Gtk.Image.new_from_icon_name(Gtk.STOCK_ABOUT, 0))
        mb_quit = Gtk.ImageMenuItem(
            'Quit', Gtk.Image.new_from_icon_name(Gtk.STOCK_QUIT, 0))

        def on_mb_about():
            self._on_about(None, None)

        def on_mb_quit():
            self._quit()

        mb_show.connect("activate", self._tray_status_event)
        mb_about.connect("activate", on_mb_about)
        mb_quit.connect("activate", on_mb_quit)

        menu.append(mb_show)
        menu.append(mb_about)
        menu.append(Gtk.SeparatorMenuItem())
        menu.append(mb_quit)
        menu.show_all()

        def pos(menu, icon):
            return Gtk.StatusIcon.position_menu(menu, icon)

        menu.popup(None, None, None, pos, button, time)

    def _error_dialog_idle(self, msg, icon=Gtk.MessageType.ERROR):
        # Thread safe
        GLib.idle_add(self._error_dialog, msg, icon)

    def _error_dialog(self, msg, icon=Gtk.MessageType.ERROR):
        def modal_close(widget, response_id):
            widget.destroy()

        dialog = Gtk.MessageDialog(self, Gtk.DialogFlags.MODAL, icon,
                                   Gtk.ButtonsType.OK, str(msg))
        dialog.show_all()
        dialog.connect("response", modal_close)
        print('Error: {}'.format(msg))

    def _on_show_action(self, main_view, event_type, selected_show, data):
        if event_type == ShowEventType.PLAY_NEXT:
            self._play_next(selected_show)
        elif event_type == ShowEventType.PLAY_EPISODE:
            self._play_episode(selected_show, data)
        elif event_type == ShowEventType.DETAILS:
            self._open_details(selected_show)
        elif event_type == ShowEventType.OPEN_WEBSITE:
            self._open_website(selected_show)
        elif event_type == ShowEventType.OPEN_FOLDER:
            self._open_folder(selected_show)
        elif event_type == ShowEventType.COPY_TITLE:
            self._copy_title(selected_show)
        elif event_type == ShowEventType.CHANGE_ALTERNATIVE_TITLE:
            self._change_alternative_title(selected_show)
        elif event_type == ShowEventType.REMOVE:
            self._remove_show(selected_show)

    def _play_next(self, show_id):
        threading.Thread(target=self._play_task, args=[show_id, True,
                                                       None]).start()

    def _play_episode(self, show_id, episode):
        threading.Thread(target=self._play_task,
                         args=[show_id, False, episode]).start()

    def _play_task(self, show_id, playnext, episode):
        self._main_view.set_buttons_sensitive_idle(False)

        show = self._engine.get_show_info(show_id)
        try:
            if playnext:
                self._engine.play_episode(show)
            else:
                if not episode:
                    episode = self.show_ep_num.get_value_as_int()
                self._engine.play_episode(show, episode)
        except utils.TrackmaError as e:
            self._error_dialog_idle(e)

        self._main_view.set_status_idle("Ready.")
        self._main_view.set_buttons_sensitive_idle(True)

    def _play_random(self):
        # TODO: Reimplement functionality in GUI
        threading.Thread(target=self._play_random_task).start()

    def _play_random_task(self):
        self._main_view.set_buttons_sensitive_idle(False)

        try:
            self._engine.play_random()
        except utils.TrackmaError as e:
            self._error_dialog_idle(e)

        self._main_view.set_status_idle("Ready.")
        self._main_view.set_buttons_sensitive_idle(True)

    def _open_details(self, show_id):
        show = self._engine.get_show_info(show_id)
        ShowInfoWindow(self._engine, show, transient_for=self)

    def _open_website(self, show_id):
        show = self._engine.get_show_info(show_id)
        if show['url']:
            Gtk.show_uri(None, show['url'], Gdk.CURRENT_TIME)

    def _open_folder(self, show_id):
        show = self._engine.get_show_info(show_id)
        try:
            filename = self._engine.get_episode_path(show, 1)
            with open(os.devnull, 'wb') as DEVNULL:
                subprocess.Popen(
                    ["/usr/bin/xdg-open",
                     os.path.dirname(filename)],
                    stdout=DEVNULL,
                    stderr=DEVNULL)
        except OSError:
            # xdg-open failed.
            raise utils.EngineError("Could not open folder.")

        except utils.EngineError:
            # Show not in library.
            self._error_dialog_idle("No folder found.")

    def _copy_title(self, show_id):
        show = self._engine.get_show_info(show_id)
        clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
        clipboard.set_text(show['title'], -1)

        self._main_view.set_status_idle('Title copied to clipboard.')

    def _change_alternative_title(self, show_id):
        show = self._engine.get_show_info(show_id)
        current_altname = self._engine.altname(show_id)

        def altname_response(entry, dialog, response):
            dialog.response(response)

        dialog = Gtk.MessageDialog(
            self, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
            Gtk.MessageType.QUESTION, Gtk.ButtonsType.OK_CANCEL, None)
        dialog.set_markup('Set the <b>alternate title</b> for the show.')
        entry = Gtk.Entry()
        entry.set_text(current_altname)
        entry.connect("activate", altname_response, dialog,
                      Gtk.ResponseType.OK)
        hbox = Gtk.HBox()
        hbox.pack_start(Gtk.Label("Alternate Title:"), False, 5, 5)
        hbox.pack_end(entry, True, True, 0)
        dialog.format_secondary_markup(
            "Use this if the tracker is unable to find this show. Leave blank to disable."
        )
        dialog.vbox.pack_end(hbox, True, True, 0)
        dialog.show_all()
        retval = dialog.run()

        if retval == Gtk.ResponseType.OK:
            text = entry.get_text()
            self._engine.altname(show_id, text)
            self._main_view.change_show_title_idle(show, text)

        dialog.destroy()

    def _remove_show(self, show_id):
        print('Window__remove_show: ', show_id)
        try:
            show = self._engine.get_show_info(show_id)
            self._engine.delete_show(show)
        except utils.TrackmaError as e:
            self._error_dialog_idle(e)