Skip to content

Commit

Permalink
Improve status messages, tooltips, etc. (#397)
Browse files Browse the repository at this point in the history
Updates #203
  • Loading branch information
JWCook committed Jul 3, 2024
2 parents a10b563 + b24d3d1 commit 4c0fe46
Show file tree
Hide file tree
Showing 10 changed files with 104 additions and 33 deletions.
6 changes: 6 additions & 0 deletions assets/data/style.qss
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ QGroupBox {
QScrollArea {
background: palette(midlight);
}
QStatusBar {
min-height: 25px;
}
QStatusBar > QLabel {
font-size: 11pt;
}

InfoCard {
background: palette(base);
Expand Down
19 changes: 14 additions & 5 deletions naturtag/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,14 @@ def __init__(self, app: NaturtagApp):
# Tabs
self.tabs = QTabWidget()
self.tabs.setIconSize(QSize(32, 32))
self.tabs.addTab(self.image_controller, fa_icon('fa.camera'), 'Photos')
self.tabs.addTab(self.taxon_controller, fa_icon('fa5s.spider'), 'Species')
self.tabs.addTab(self.observation_controller, fa_icon('fa5s.binoculars'), 'Observations')
idx = self.tabs.addTab(self.image_controller, fa_icon('fa.camera'), 'Photos')
self.tabs.setTabToolTip(idx, 'Add and tag local photos')
idx = self.tabs.addTab(self.taxon_controller, fa_icon('fa5s.spider'), 'Species')
self.tabs.setTabToolTip(idx, 'Browse and search taxonomy')
idx = self.tabs.addTab(
self.observation_controller, fa_icon('fa5s.binoculars'), 'Observations'
)
self.tabs.setTabToolTip(idx, 'Browse your recent observations')

# Root layout: tabs + progress bar
self.root_widget = QWidget()
Expand All @@ -110,6 +115,7 @@ def __init__(self, app: NaturtagApp):
self.app.log_handler.widget, fa_icon('fa.file-text-o'), 'Logs'
)
self.tabs.setTabVisible(self.log_tab_idx, self.app.settings.show_logs)
self.tabs.setTabToolTip(self.log_tab_idx, 'View application logs')

# Photos tab: view taxon and switch tab
self.image_controller.gallery.on_view_taxon_id.connect(
Expand Down Expand Up @@ -179,6 +185,9 @@ def __init__(self, app: NaturtagApp):
self.addToolBar(self.toolbar)
self.statusbar = QStatusBar(self)
self.setStatusBar(self.statusbar)
# self.status_widget = QLabel('This is a status widget')
# self.statusbar.addWidget(self.status_widget)
# self.status_widget.setAttribute(Qt.WA_TransparentForMouseEvents)

# Load any valid image paths provided on command line (or from drag & drop)
self.image_controller.gallery.load_images(
Expand Down Expand Up @@ -212,9 +221,9 @@ def closeEvent(self, _):
self.app.settings.write()
self.app.state.write()

def info(self, message: str):
def info(self, message: str, timeout: int = 3000):
"""Show a message both in the status bar and in the logs"""
self.statusbar.showMessage(message)
self.statusbar.showMessage(message, timeout)
logger.info(message)

def mousePressEvent(self, event):
Expand Down
4 changes: 2 additions & 2 deletions naturtag/app/controls.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def add_button(
) -> QAction:
action = QAction(fa_icon(icon), name, self)
if tooltip:
action.setStatusTip(tooltip)
action.setToolTip(tooltip)
if shortcut:
action.setShortcut(QKeySequence(shortcut))
if visible:
Expand Down Expand Up @@ -198,8 +198,8 @@ def add_favorite_dir(self, image_dir: Path, save: bool = True) -> Optional[QActi
return None
if save:
self.settings.add_favorite_dir(image_dir)
logger.debug(f'Adding favorite: {image_dir}')

logger.debug(f'Adding favorite: {image_dir}')
action = self.favorite_dirs_submenu.addAction(
fa_icon('mdi.folder-star'),
str(image_dir).replace(HOME_DIR, '~'),
Expand Down
3 changes: 3 additions & 0 deletions naturtag/controllers/base_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ class BaseController(StylableWidget):
@property
def app(self) -> 'NaturtagApp':
return QApplication.instance()

def info(self, message: str):
self.on_message.emit(message)
36 changes: 22 additions & 14 deletions naturtag/controllers/image_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
TaxonInfoCard,
VerticalLayout,
)
from naturtag.widgets.images import FAIcon

logger = getLogger(__name__)

Expand All @@ -34,6 +35,26 @@ def __init__(self):
top_section_layout.setAlignment(Qt.AlignLeft)
photo_layout.addLayout(top_section_layout)
self.on_new_metadata.connect(self.update_metadata)
self.selected_taxon_id: Optional[int] = None
self.selected_observation_id: Optional[int] = None

# Selected taxon/observation info
group_box = QGroupBox('Metadata source')
group_box.setFixedHeight(150)
group_box.setMinimumWidth(400)
group_box.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum)
top_section_layout.addWidget(group_box)
self.data_source_card = HorizontalLayout(group_box)
self.data_source_card.setAlignment(Qt.AlignLeft)

# Help text
help_msg = QLabel(
'Select a source of metadata to tag photos with.\n'
'Browse the Species or Observations tabs,\n'
'paste an iNaturalist URL, or enter an ID to the right.'
)
self.data_source_card.addWidget(FAIcon('ei.info-circle'))
self.data_source_card.addWidget(help_msg)

# Input group
group_box = QGroupBox('Quick entry')
Expand All @@ -52,16 +73,6 @@ def __init__(self):
inputs_layout.addWidget(QLabel('Observation ID:'))
inputs_layout.addWidget(self.input_obs_id)

# Selected taxon/observation info
group_box = QGroupBox('Metadata source')
group_box.setFixedHeight(150)
group_box.setMinimumWidth(400)
group_box.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum)
top_section_layout.addWidget(group_box)
self.data_source_card = HorizontalLayout(group_box)
self.selected_taxon_id: Optional[int] = None
self.selected_observation_id: Optional[int] = None

# Image gallery
self.gallery = ImageGallery()
photo_layout.addWidget(self.gallery)
Expand Down Expand Up @@ -108,12 +119,12 @@ def refresh(self):
self.info('Select images to tag')
return

self.info(f'Refreshing tags for {len(images)} images')
for image in images:
future = self.app.threadpool.schedule(
lambda: _refresh_tags(image.metadata, self.app.client, self.app.settings),
)
future.on_result.connect(self.update_metadata)
self.info(f'{len(images)} images updated')

def clear(self):
"""Clear all images and input"""
Expand Down Expand Up @@ -197,6 +208,3 @@ def select_observation(self, observation: Observation):
card = ObservationInfoCard(obs=observation, delayed_load=False)
card.on_click.connect(self.on_view_observation_id)
self.data_source_card.addWidget(card)

def info(self, message: str):
self.on_message.emit(message)
18 changes: 18 additions & 0 deletions naturtag/controllers/image_gallery.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,20 @@ def __init__(self):
scroll_area.setWidget(self.scroll_panel)
root.addWidget(scroll_area)

# Help text
help = QWidget()
help.setContentsMargins(5, 10, 5, 10)
help_layout = HorizontalLayout(help)
help_layout.setAlignment(Qt.AlignLeft)
self.flow_layout.addWidget(help)
help_msg = QLabel(
'Select local photos to tag.\n'
'Drag and drop files onto the window,\n'
'Or use the file browser (Ctrl+O).'
)
help_layout.addWidget(FAIcon('ei.info-circle'))
help_layout.addWidget(help_msg)

def clear(self):
"""Clear all images from the viewer"""
self.images = {}
Expand Down Expand Up @@ -112,6 +126,10 @@ def load_image(self, image_path: Path, delayed_load: bool = False) -> Optional['
logger.debug(f'Image already loaded: {image_path}')
return None

# Clear initial help text if still present
if not self.images:
self.flow_layout.clear()

logger.info(f'Loading {image_path}')
thumbnail_card = ThumbnailCard(image_path)
thumbnail_card.on_loaded.connect(self._bind_image_actions)
Expand Down
3 changes: 2 additions & 1 deletion naturtag/controllers/observation_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def load_user_observations(self):
logger.info('Unknown user; skipping observation load')
return

logger.info('Fetching user observations')
self.info('Fetching user observations')
future = self.app.threadpool.schedule(
self.get_user_observations, priority=QThread.LowPriority
)
Expand Down Expand Up @@ -139,6 +139,7 @@ def display_user_observations(self, observations: list[Observation]):
self.update_pagination_buttons()
if self.total_results:
self.user_obs_group_box.set_title(f'My Observations ({self.total_results})')
self.info('')

def bind_selection(self, obs_cards: Iterable[ObservationInfoCard]):
"""Connect click signal from each observation card"""
Expand Down
5 changes: 3 additions & 2 deletions naturtag/controllers/taxon_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def display_taxon_by_id(self, taxon_id: int):
return

# Fetch taxon record
logger.info(f'Loading taxon {taxon_id}')
self.info(f'Loading taxon {taxon_id}')
client = self.app.client
if self.tabs._init_complete:
self.app.threadpool.cancel()
Expand All @@ -96,6 +96,7 @@ def display_taxon(self, taxon: Taxon, notify: bool = True):
self.bind_selection(self.taxonomy.ancestors_list.cards)
self.bind_selection(self.taxonomy.children_list.cards)
logger.debug(f'Loaded taxon {taxon.id}')
self.info('')

def set_search_results(self, taxa: list[Taxon]):
"""Load search results into Results tab"""
Expand Down Expand Up @@ -163,7 +164,7 @@ def add_tab(self, taxon_list: TaxonList, icon_str: str, label: str, tooltip: str
return taxon_list

def load_user_taxa(self):
logger.info('Fetching user-observed taxa')
self.info('Fetching user-observed taxa')
app = get_app()
client = app.client
display_ids = self.user_taxa.display_ids
Expand Down
7 changes: 3 additions & 4 deletions naturtag/storage/app_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,8 @@

@define(auto_attribs=False, slots=False)
class AppState:
"""Application state container. This includes values that don't need to be
human-readable/editable; so, they don't need to be stored in settings.yml, and are persisted in
SQLite instead.
"""Container for persistent application state info. This includes values that don't need to be
human-readable/editable; so, they and are persisted in SQLite instead of `settings.yml`.
"""

db_path: Path = None # type: ignore
Expand Down Expand Up @@ -117,7 +116,7 @@ def __str__(self):
@classmethod
def read(cls, db_path: Path = DB_PATH) -> 'AppState':
"""Read app state from SQLite database, or return a new instance if no state is found"""
logger.info(f'Reading app state from {db_path}')
logger.debug(f'Reading app state from {db_path}')

try:
with get_session(db_path) as session:
Expand Down
36 changes: 31 additions & 5 deletions naturtag/storage/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def from_ids(

# Add full taxonomy to observations, if specified
if taxonomy:
self.taxon_controller._get_taxonomy([obs.taxon for obs in observations])
self.taxon_controller._add_taxonomy([obs.taxon for obs in observations])

logger.debug(f'Finished in {time()-start:.2f} seconds')
return WrapperPaginator(observations)
Expand Down Expand Up @@ -126,7 +126,10 @@ def get_user_observations(
updated_since=updated_since,
refresh=True,
).all()
logger.debug(f'{len(new_observations)} new observations found')
if new_observations:
logger.info(f'{len(new_observations)} new observations found since {updated_since}')
else:
logger.info(f'No new observations found since {updated_since}')

if not limit:
return []
Expand Down Expand Up @@ -178,6 +181,7 @@ def from_ids(
api_results = super().from_ids(remaining_ids, **params).all() if remaining_ids else []
taxa.extend(api_results)
save_taxa(api_results, self.client.db_path)
api_results = self._add_db_taxonomy(api_results)

logger.debug(f'Finished in {time()-start:.2f} seconds')
return WrapperPaginator(taxa)
Expand All @@ -187,13 +191,13 @@ def _get_db_taxa(self, taxon_ids: list[int], accept_partial: bool = False):
get_db_taxa(self.client.db_path, ids=taxon_ids, accept_partial=accept_partial)
)
if not accept_partial:
db_results = self._get_taxonomy(db_results)
db_results = self._add_taxonomy(db_results)
return db_results

def _get_taxonomy(self, taxa: list[Taxon]) -> list[Taxon]:
def _add_taxonomy(self, taxa: list[Taxon]) -> list[Taxon]:
"""Add ancestor and descendant records to all the specified taxa.
DB records only contain ancestor/child IDs, so we need another query to fetch full records
DB records only contain ancestor/child IDs, so we need another query to fetch full records.
This could be done in SQL, but a many-to-many relationship with ancestors would get messy.
Besides, some may be missing and need to be fetched from the API.
"""
Expand All @@ -211,6 +215,28 @@ def _get_taxonomy(self, taxa: list[Taxon]) -> list[Taxon]:
taxon.children = [extended_taxa[id] for id in taxon.child_ids]
return taxa

def _add_db_taxonomy(self, taxa: list[Taxon]) -> list[Taxon]:
"""Given new taxon records from the API, replace partial ancestors and children with any
that are stored locally. This is mainly for displaying aggregate values in the taxonomy
browser.
"""
api_taxa = chain.from_iterable([t.ancestors + t.children for t in taxa])
partial_taxa = {t.id: t for t in api_taxa}
full_taxa = {
t.id: t
for t in get_db_taxa(
self.client.db_path, ids=list(partial_taxa.keys()), accept_partial=True
)
}
for taxon in taxa:
taxon.ancestors = [
full_taxa.get(id) or partial_taxa[id]
for id in taxon.ancestor_ids
if id not in [ROOT_TAXON_ID, taxon.id]
]
taxon.children = [full_taxa.get(id) or partial_taxa[id] for id in taxon.child_ids]
return taxa

# TODO: Save one page at a time
def search(self, **params) -> WrapperPaginator[Taxon]:
"""Search taxa, and save results to the database (for future reference by ID)"""
Expand Down

0 comments on commit 4c0fe46

Please sign in to comment.