StretchHeaderView: Refactor code and improve header view
Save what sections are visible, and always save sizes. Do not set section size to zero when hiding sections. When resizing columns in stretch mode, only resize the right column to fit the left column. Fixes #1085
This commit is contained in:
parent
6ba1fdb744
commit
b8ef96028c
|
@ -2,7 +2,7 @@
|
|||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -35,18 +35,20 @@
|
|||
|
||||
#include "stretchheaderview.h"
|
||||
|
||||
const int StretchHeaderView::kMinimumColumnWidth = 10;
|
||||
const int StretchHeaderView::kMagicNumber = 0x502c950f;
|
||||
namespace {
|
||||
constexpr int kMagicNumber = 0x502C9510;
|
||||
}
|
||||
|
||||
StretchHeaderView::StretchHeaderView(const Qt::Orientation orientation, QWidget *parent)
|
||||
: QHeaderView(orientation, parent),
|
||||
stretch_enabled_(false),
|
||||
in_mouse_move_event_(false) {
|
||||
|
||||
QObject::connect(this, &StretchHeaderView::sectionResized, this, &StretchHeaderView::SectionResized);
|
||||
setMinimumSectionSize(kMinimumColumnWidth);
|
||||
setTextElideMode(Qt::ElideRight);
|
||||
setDefaultSectionSize(100);
|
||||
setMinimumSectionSize(30);
|
||||
setTextElideMode(Qt::ElideRight);
|
||||
|
||||
QObject::connect(this, &StretchHeaderView::sectionResized, this, &StretchHeaderView::SectionResized);
|
||||
|
||||
}
|
||||
|
||||
|
@ -61,122 +63,13 @@ void StretchHeaderView::setModel(QAbstractItemModel *model) {
|
|||
|
||||
}
|
||||
|
||||
void StretchHeaderView::NormaliseWidths(const QList<int> §ions) {
|
||||
|
||||
if (!stretch_enabled_) return;
|
||||
|
||||
const ColumnWidthType total_sum = std::accumulate(column_widths_.begin(), column_widths_.end(), 0.0);
|
||||
ColumnWidthType selected_sum = total_sum;
|
||||
|
||||
if (!sections.isEmpty()) {
|
||||
selected_sum = 0.0;
|
||||
for (int i = 0; i < count(); ++i)
|
||||
if (sections.contains(i)) {
|
||||
selected_sum += column_widths_[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (total_sum != 0.0 && !qFuzzyCompare(total_sum, 1.0)) {
|
||||
const ColumnWidthType mult = (selected_sum + (1.0 - total_sum)) / selected_sum;
|
||||
for (int i = 0; i < column_widths_.count(); ++i) {
|
||||
if (sections.isEmpty() || sections.contains(i)) {
|
||||
column_widths_[i] *= mult;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void StretchHeaderView::UpdateWidths(const QList<int> §ions) {
|
||||
|
||||
if (!stretch_enabled_) return;
|
||||
|
||||
for (int i = 0; i < column_widths_.count(); ++i) {
|
||||
const ColumnWidthType w = column_widths_[i];
|
||||
|
||||
int pixels = static_cast<int>(w * width());
|
||||
|
||||
if (!sections.isEmpty() && !sections.contains(i)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pixels == 0 && !isSectionHidden(i)) {
|
||||
hideSection(i);
|
||||
}
|
||||
else if (pixels != 0 && isSectionHidden(i)) {
|
||||
showSection(i);
|
||||
}
|
||||
|
||||
if (pixels != 0) {
|
||||
resizeSection(i, pixels);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void StretchHeaderView::HideSection(const int logical) {
|
||||
|
||||
// Would this hide the last section?
|
||||
bool all_hidden = true;
|
||||
for (int i = 0; i < count(); ++i) {
|
||||
if (i != logical && !isSectionHidden(i) && sectionSize(i) > 0) {
|
||||
all_hidden = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (all_hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stretch_enabled_) {
|
||||
hideSection(logical);
|
||||
return;
|
||||
}
|
||||
|
||||
column_widths_[logical] = 0.0;
|
||||
NormaliseWidths();
|
||||
UpdateWidths();
|
||||
|
||||
}
|
||||
|
||||
void StretchHeaderView::ShowSection(int logical) {
|
||||
|
||||
if (!stretch_enabled_) {
|
||||
showSection(logical);
|
||||
return;
|
||||
}
|
||||
|
||||
// How many sections are visible already?
|
||||
int visible_count = 0;
|
||||
for (int i = 0; i < count(); ++i) {
|
||||
if (!isSectionHidden(i)) {
|
||||
++visible_count;
|
||||
}
|
||||
}
|
||||
|
||||
column_widths_[logical] = visible_count == 0 ? 1.0 : 1.0 / visible_count;
|
||||
NormaliseWidths();
|
||||
UpdateWidths();
|
||||
|
||||
}
|
||||
|
||||
void StretchHeaderView::SetSectionHidden(const int logical, const bool hidden) {
|
||||
|
||||
if (hidden) {
|
||||
HideSection(logical);
|
||||
}
|
||||
else {
|
||||
ShowSection(logical);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void StretchHeaderView::resizeEvent(QResizeEvent *e) {
|
||||
|
||||
QHeaderView::resizeEvent(e);
|
||||
|
||||
if (!stretch_enabled_) return;
|
||||
|
||||
UpdateWidths();
|
||||
if (stretch_enabled_) {
|
||||
ResizeSections();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -188,31 +81,101 @@ void StretchHeaderView::mouseMoveEvent(QMouseEvent *e) {
|
|||
|
||||
}
|
||||
|
||||
void StretchHeaderView::SectionResized(const int logical, const int, const int new_size) {
|
||||
QByteArray StretchHeaderView::SaveState() const {
|
||||
|
||||
if (!stretch_enabled_) return;
|
||||
|
||||
if (in_mouse_move_event_) {
|
||||
// Update this section's proportional width
|
||||
column_widths_[logical] = static_cast<ColumnWidthType>(new_size) / width();
|
||||
|
||||
// Find the visible sections to the right of the section that's being resized
|
||||
int visual = visualIndex(logical);
|
||||
QList<int> logical_sections_to_resize;
|
||||
QList<int> visual_indices;
|
||||
QList<int> column_pixel_widths;
|
||||
QList<int> columns_visible;
|
||||
visual_indices.reserve(count());
|
||||
column_pixel_widths.reserve(count());
|
||||
for (int i = 0; i < count(); ++i) {
|
||||
if (!isSectionHidden(i) && visualIndex(i) > visual) {
|
||||
logical_sections_to_resize << i;
|
||||
visual_indices << logicalIndex(i);
|
||||
column_pixel_widths << sectionSize(i);
|
||||
if (!isSectionHidden(i)) {
|
||||
columns_visible << i;
|
||||
}
|
||||
}
|
||||
|
||||
// Resize just those columns
|
||||
if (!logical_sections_to_resize.isEmpty()) {
|
||||
in_mouse_move_event_ = false;
|
||||
UpdateWidths(logical_sections_to_resize);
|
||||
NormaliseWidths(logical_sections_to_resize);
|
||||
in_mouse_move_event_ = true;
|
||||
QByteArray state;
|
||||
QDataStream s(&state, QIODevice::WriteOnly);
|
||||
s.setVersion(QDataStream::Qt_5_12);
|
||||
s << kMagicNumber;
|
||||
s << stretch_enabled_;
|
||||
s << visual_indices;
|
||||
s << column_pixel_widths;
|
||||
s << columns_visible;
|
||||
s << column_widths_;
|
||||
s << static_cast<int>(sortIndicatorOrder());
|
||||
s << sortIndicatorSection();
|
||||
|
||||
return state;
|
||||
|
||||
}
|
||||
|
||||
bool StretchHeaderView::RestoreState(const QByteArray &state) {
|
||||
|
||||
if (state.isEmpty()) return false;
|
||||
|
||||
int magic_number = 0;
|
||||
QList<int> visual_indices;
|
||||
QList<int> column_pixel_widths;
|
||||
QList<int> columns_visible;
|
||||
int sort_indicator_order = Qt::AscendingOrder;
|
||||
int sort_indicator_section = 0;
|
||||
|
||||
QDataStream s(state);
|
||||
s.setVersion(QDataStream::Qt_5_12);
|
||||
|
||||
s >> magic_number;
|
||||
|
||||
if (magic_number != kMagicNumber || s.atEnd()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
s >> stretch_enabled_;
|
||||
s >> visual_indices;
|
||||
s >> column_pixel_widths;
|
||||
s >> columns_visible;
|
||||
s >> column_widths_;
|
||||
s >> sort_indicator_order;
|
||||
s >> sort_indicator_section;
|
||||
|
||||
for (int i = 0; i < count(); ++i) {
|
||||
if (i < visual_indices.count()) {
|
||||
moveSection(visualIndex(visual_indices[i]), i);
|
||||
}
|
||||
if (i < column_pixel_widths.count()) {
|
||||
resizeSection(i, column_pixel_widths[i]);
|
||||
}
|
||||
setSectionHidden(i, !columns_visible.contains(i));
|
||||
}
|
||||
|
||||
// Have we added more columns since the last time?
|
||||
while (column_widths_.count() < count()) {
|
||||
column_widths_ << 0;
|
||||
}
|
||||
|
||||
setSortIndicator(sort_indicator_section, static_cast<Qt::SortOrder>(sort_indicator_order));
|
||||
|
||||
if (stretch_enabled_) {
|
||||
// In stretch mode, we've already set the proportional column widths so apply them now.
|
||||
ResizeSections();
|
||||
}
|
||||
|
||||
emit StretchEnabledChanged(stretch_enabled_);
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
QByteArray StretchHeaderView::ResetState() {
|
||||
|
||||
stretch_enabled_ = true;
|
||||
|
||||
setSortIndicator(0, Qt::AscendingOrder);
|
||||
|
||||
return QByteArray();
|
||||
|
||||
}
|
||||
|
||||
void StretchHeaderView::ToggleStretchEnabled() {
|
||||
|
@ -232,22 +195,22 @@ void StretchHeaderView::SetStretchEnabled(const bool enabled) {
|
|||
|
||||
// Stretch the columns to fill the widget
|
||||
NormaliseWidths();
|
||||
UpdateWidths();
|
||||
ResizeSections();
|
||||
}
|
||||
|
||||
emit StretchEnabledChanged(enabled);
|
||||
|
||||
}
|
||||
|
||||
void StretchHeaderView::SetColumnWidth(const int logical, const ColumnWidthType width) {
|
||||
void StretchHeaderView::SetColumnWidth(const int logical_index, const ColumnWidthType width) {
|
||||
|
||||
column_widths_[logical_index] = width;
|
||||
|
||||
if (!stretch_enabled_) return;
|
||||
|
||||
column_widths_[logical] = width;
|
||||
|
||||
QList<int> other_columns;
|
||||
for (int i = 0; i < count(); ++i) {
|
||||
if (!isSectionHidden(i) && i != logical) {
|
||||
if (!isSectionHidden(i) && i != logical_index) {
|
||||
other_columns << i;
|
||||
}
|
||||
}
|
||||
|
@ -256,125 +219,146 @@ void StretchHeaderView::SetColumnWidth(const int logical, const ColumnWidthType
|
|||
|
||||
}
|
||||
|
||||
bool StretchHeaderView::RestoreState(const QByteArray &sdata) {
|
||||
void StretchHeaderView::NormaliseWidths(const QList<int> §ions) {
|
||||
|
||||
QDataStream s(sdata);
|
||||
s.setVersion(QDataStream::Qt_5_6);
|
||||
if (!stretch_enabled_) return;
|
||||
|
||||
int magic_number = 0;
|
||||
s >> magic_number;
|
||||
const ColumnWidthType total_sum = std::accumulate(column_widths_.begin(), column_widths_.end(), 0.0);
|
||||
ColumnWidthType selected_sum = total_sum;
|
||||
|
||||
if (magic_number != kMagicNumber || s.atEnd()) {
|
||||
return false;
|
||||
if (!sections.isEmpty()) {
|
||||
selected_sum = 0.0;
|
||||
for (int i = 0; i < count(); ++i) {
|
||||
if (sections.contains(i)) {
|
||||
selected_sum += column_widths_[i];
|
||||
}
|
||||
|
||||
QList<int> pixel_widths;
|
||||
QList<int> visual_indices;
|
||||
int sort_indicator_order = Qt::AscendingOrder;
|
||||
int sort_indicator_section = 0;
|
||||
|
||||
s >> stretch_enabled_;
|
||||
s >> pixel_widths;
|
||||
s >> visual_indices;
|
||||
s >> column_widths_;
|
||||
s >> sort_indicator_order;
|
||||
s >> sort_indicator_section;
|
||||
|
||||
setSortIndicator(sort_indicator_section, static_cast<Qt::SortOrder>(sort_indicator_order));
|
||||
|
||||
const qint64 persisted_column_count = qMin(qMin(visual_indices.count(), pixel_widths.count()), column_widths_.count());
|
||||
|
||||
// Set column visible state, visual indices and, if we're not in stretch mode, pixel widths.
|
||||
for (int i = 0; i < count() && i < persisted_column_count; ++i) {
|
||||
setSectionHidden(i, pixel_widths[i] <= kMinimumColumnWidth);
|
||||
moveSection(visualIndex(visual_indices[i]), i);
|
||||
|
||||
if (!stretch_enabled_) {
|
||||
resizeSection(i, pixel_widths[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Have we added more columns since the last time?
|
||||
while (column_widths_.count() < count()) {
|
||||
column_widths_ << 0;
|
||||
if (total_sum != 0.0 && !qFuzzyCompare(total_sum, 1.0)) {
|
||||
const ColumnWidthType mult = (selected_sum + (1.0 - total_sum)) / selected_sum;
|
||||
for (int i = 0; i < column_widths_.count(); ++i) {
|
||||
if (sections.isEmpty() || sections.contains(i)) {
|
||||
column_widths_[i] *= mult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void StretchHeaderView::ResizeSections(const QList<int> §ions) {
|
||||
|
||||
if (!stretch_enabled_) return;
|
||||
|
||||
for (int i = 0; i < column_widths_.count(); ++i) {
|
||||
if (isSectionHidden(i) || (!sections.isEmpty() && !sections.contains(i))) {
|
||||
continue;
|
||||
}
|
||||
const int pixels = static_cast<int>(column_widths_[i] * width());
|
||||
if (pixels != 0) {
|
||||
resizeSection(i, pixels);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void StretchHeaderView::ShowSection(const int logical_index) {
|
||||
|
||||
showSection(logical_index);
|
||||
|
||||
if (stretch_enabled_) {
|
||||
// In stretch mode, we've already set the proportional column widths so apply them now.
|
||||
UpdateWidths();
|
||||
}
|
||||
|
||||
emit StretchEnabledChanged(stretch_enabled_);
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
QByteArray StretchHeaderView::SaveState() const {
|
||||
|
||||
QByteArray ret;
|
||||
QDataStream s(&ret, QIODevice::WriteOnly);
|
||||
|
||||
QList<int> pixel_widths;
|
||||
QList<int> visual_indices;
|
||||
|
||||
pixel_widths.reserve(count());
|
||||
visual_indices.reserve(count());
|
||||
|
||||
// How many sections are visible already?
|
||||
int visible_count = 0;
|
||||
for (int i = 0; i < count(); ++i) {
|
||||
pixel_widths << sectionSize(i);
|
||||
visual_indices << logicalIndex(i);
|
||||
if (!isSectionHidden(i)) {
|
||||
++visible_count;
|
||||
}
|
||||
}
|
||||
|
||||
s.setVersion(QDataStream::Qt_5_6);
|
||||
column_widths_[logical_index] = visible_count == 0 ? 1.0 : 1.0 / visible_count;
|
||||
|
||||
s << kMagicNumber;
|
||||
NormaliseWidths();
|
||||
ResizeSections();
|
||||
|
||||
s << stretch_enabled_;
|
||||
s << pixel_widths;
|
||||
s << visual_indices;
|
||||
s << column_widths_;
|
||||
s << static_cast<int>(sortIndicatorOrder());
|
||||
s << sortIndicatorSection();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QByteArray StretchHeaderView::ResetState() {
|
||||
|
||||
QByteArray ret;
|
||||
|
||||
QDataStream s(&ret, QIODevice::WriteOnly);
|
||||
s.setVersion(QDataStream::Qt_5_6);
|
||||
|
||||
s << kMagicNumber;
|
||||
|
||||
stretch_enabled_ = true;
|
||||
|
||||
column_widths_.resize(count());
|
||||
std::fill(column_widths_.begin(), column_widths_.end(), 1.0 / count());
|
||||
|
||||
QList<int> visual_indices;
|
||||
QList<int> pixel_widths;
|
||||
|
||||
visual_indices.reserve(count());
|
||||
pixel_widths.reserve(count());
|
||||
void StretchHeaderView::HideSection(const int logical_index) {
|
||||
|
||||
// Would this hide the last section?
|
||||
bool all_hidden = true;
|
||||
for (int i = 0; i < count(); ++i) {
|
||||
pixel_widths << 10;
|
||||
visual_indices << count();
|
||||
if (i != logical_index && !isSectionHidden(i) && sectionSize(i) > 0) {
|
||||
all_hidden = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (all_hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
s << stretch_enabled_;
|
||||
s << pixel_widths;
|
||||
s << visual_indices;
|
||||
s << column_widths_;
|
||||
s << static_cast<int>(Qt::AscendingOrder);
|
||||
s << 0;
|
||||
hideSection(logical_index);
|
||||
|
||||
RestoreState(ret);
|
||||
|
||||
return ret;
|
||||
if (stretch_enabled_) {
|
||||
column_widths_[logical_index] = 0.0;
|
||||
NormaliseWidths();
|
||||
ResizeSections();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void StretchHeaderView::SetSectionHidden(const int logical_index, const bool hidden) {
|
||||
|
||||
if (hidden) {
|
||||
HideSection(logical_index);
|
||||
}
|
||||
else {
|
||||
ShowSection(logical_index);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void StretchHeaderView::SectionResized(const int logical_index, const int old_size, const int new_size) {
|
||||
|
||||
if (!stretch_enabled_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (in_mouse_move_event_) {
|
||||
bool resized = false;
|
||||
if (new_size >= minimumSectionSize()) {
|
||||
// Find the visible section to the right of the section that's being resized
|
||||
const int visual_index = visualIndex(logical_index);
|
||||
int right_section_logical_index = -1;
|
||||
int right_section_visual_index = -1;
|
||||
for (int i = 0; i <= count(); ++i) {
|
||||
if (!isSectionHidden(i) &&
|
||||
visualIndex(i) > visual_index &&
|
||||
(right_section_visual_index == -1 || visualIndex(i) < right_section_visual_index)) {
|
||||
right_section_logical_index = i;
|
||||
right_section_visual_index = visualIndex(i);
|
||||
}
|
||||
}
|
||||
if (right_section_logical_index != -1) {
|
||||
const int right_section_size = sectionSize(right_section_logical_index) + (old_size - new_size);
|
||||
if (right_section_size >= minimumSectionSize()) {
|
||||
column_widths_[logical_index] = static_cast<ColumnWidthType>(new_size) / width();
|
||||
column_widths_[right_section_logical_index] = static_cast<ColumnWidthType>(right_section_size) / width();
|
||||
in_mouse_move_event_ = false;
|
||||
NormaliseWidths(QList<int>() << right_section_logical_index);
|
||||
ResizeSections(QList<int>() << right_section_logical_index);
|
||||
in_mouse_move_event_ = true;
|
||||
resized = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!resized) {
|
||||
in_mouse_move_event_ = true;
|
||||
resizeSection(logical_index, old_size);
|
||||
in_mouse_move_event_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -42,31 +42,28 @@ class StretchHeaderView : public QHeaderView {
|
|||
|
||||
using ColumnWidthType = double;
|
||||
|
||||
static const int kMinimumColumnWidth;
|
||||
static const int kMagicNumber;
|
||||
|
||||
void setModel(QAbstractItemModel *model) override;
|
||||
|
||||
bool is_stretch_enabled() const { return stretch_enabled_; }
|
||||
|
||||
// Serialises the proportional and actual column widths.
|
||||
// Use these instead of QHeaderView::restoreState and QHeaderView::saveState to persist the proportional values directly and avoid floating point errors over time.
|
||||
bool RestoreState(const QByteArray &sdata);
|
||||
QByteArray SaveState() const;
|
||||
QByteArray ResetState();
|
||||
|
||||
// Hides a section and resizes all other sections to fill the gap. Does nothing if you try to hide the last section.
|
||||
void HideSection(const int logical);
|
||||
|
||||
// Shows a section and resizes all other sections to make room.
|
||||
void ShowSection(const int logical);
|
||||
void ShowSection(const int logical_index);
|
||||
|
||||
// Hides a section and resizes all other sections to fill the gap. Does nothing if you try to hide the last section.
|
||||
void HideSection(const int logical_index);
|
||||
|
||||
// Calls either HideSection or ShowSection.
|
||||
void SetSectionHidden(const int logical, const bool hidden);
|
||||
void SetSectionHidden(const int logical_index, const bool hidden);
|
||||
|
||||
// Sets the width of the given column and resizes other columns appropriately.
|
||||
// width is the proportion of the entire width from 0.0 to 1.0.
|
||||
void SetColumnWidth(const int logical, const ColumnWidthType width);
|
||||
|
||||
bool is_stretch_enabled() const { return stretch_enabled_; }
|
||||
void SetColumnWidth(const int logical_index, const ColumnWidthType width);
|
||||
|
||||
public slots:
|
||||
// Changes the stretch mode.
|
||||
|
@ -88,7 +85,7 @@ class StretchHeaderView : public QHeaderView {
|
|||
void NormaliseWidths(const QList<int> §ions = QList<int>());
|
||||
|
||||
// Resizes the actual columns to make them match the proportional values in column_widths_.
|
||||
void UpdateWidths(const QList<int> §ions = QList<int>());
|
||||
void ResizeSections(const QList<int> §ions = QList<int>());
|
||||
|
||||
private slots:
|
||||
void SectionResized(const int logical, const int old_size, const int new_size);
|
||||
|
|
Loading…
Reference in New Issue